Skip to content

Commit

Permalink
Merge pull request #8 from asantibanez/features/query-builder-methods
Browse files Browse the repository at this point in the history
Add ability to constraint query builder results using state machine state history
  • Loading branch information
asantibanez authored Dec 21, 2020
2 parents 1c1a0c4 + a885c3a commit e0ecafb
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to `laravel-eloquent-state-machines` will be documented in this file

## v2.2.0 - 2020-12-21

- Added macros on query builder to interact with `state_history`

## v2.1.2 - 2020-12-16

- Added auth()->user() in state history during model creation
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,37 @@ $salesOrder->status()->history()
->get();
```

### Using Query Builder

The `HasStateMachines` trait introduces a helper method when querying your models based on the state history of each
state machine. You can use the `whereHas{FIELD_NAME}` (eg: `whereHasStatus`, `whereHasFulfillment`) to add constraints
to your model queries depending on state transitions, responsible and custom properties.

The `whereHas{FIELD_NAME}` method accepts a closure where you can add the following type of constraints:

- `withTransition($from, $to)`
- `transitionedFrom($to)`
- `transitionedTo($to)`
- `withResponsible($responsible|$id)`
- `withCustomProperty($property, $operator, $value)`

```php
SalesOrder::with()
->whereHasStatus(function ($query) {
$query
->withTransition('pending', 'approved')
->withResponsible(auth()->id())
;
})
->whereHasFulfillment(function ($query) {
$query
->transitionedTo('complete')
;
})
->get();
```


### Getting Custom Properties

When applying transitions with custom properties, we can get our registered values using the
Expand Down
27 changes: 27 additions & 0 deletions src/Models/StateHistory.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,40 @@ public function scopeFrom($query, $from)
$query->where('from', $from);
}

public function scopeTransitionedFrom($query, $from)
{
$query->from($from);
}

public function scopeTo($query, $to)
{
$query->where('to', $to);
}

public function scopeTransitionedTo($query, $to)
{
$query->to($to);
}

public function scopeWithTransition($query, $from, $to)
{
$query->from($from)->to($to);
}

public function scopeWithCustomProperty($query, $key, $operator, $value = null)
{
$query->where("custom_properties->{$key}", $operator, $value);
}

public function scopeWithResponsible($query, $responsible)
{
if ($responsible instanceof Model) {
return $query
->where('responsible_id', $responsible->getKey())
->where('responsible_type', get_class($responsible))
;
}

return $query->where('responsible_id', $responsible);
}
}
23 changes: 22 additions & 1 deletion src/Traits/HasStateMachines.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
use Asantibanez\LaravelEloquentStateMachines\Models\PendingTransition;
use Asantibanez\LaravelEloquentStateMachines\Models\StateHistory;
use Asantibanez\LaravelEloquentStateMachines\StateMachines\State;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Javoscript\MacroableModels\Facades\MacroableModels;
use Str;

/**
* Trait HasStateMachines
Expand All @@ -21,10 +23,29 @@ public static function bootHasStateMachines()

collect($model->stateMachines)
->each(function ($_, $field) use ($model) {
MacroableModels::addMacro(static::class, "$field", function () use ($field) {
$camelField = Str::of($field)->camel();

MacroableModels::addMacro(static::class, $camelField, function () use ($field) {
$stateMachine = new $this->stateMachines[$field]($field, $this);
return new State($this->{$stateMachine->field}, $stateMachine);
});


$studlyField = Str::of($field)->studly();

Builder::macro("whereHas{$studlyField}", function ($callable) use ($field) {
$model = $this->getModel();

if (!method_exists($model, 'stateHistory')) {
return $this->newQuery();
}

return $this->whereHas('stateHistory', function ($query) use ($field, $callable) {
$query->forField($field);
$callable($query);
return $query;
});
});
});

self::creating(function (Model $model) {
Expand Down
195 changes: 195 additions & 0 deletions tests/Feature/QueryScopesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<?php

namespace Asantibanez\LaravelEloquentStateMachines\Tests\Feature;

use Asantibanez\LaravelEloquentStateMachines\Tests\TestCase;
use Asantibanez\LaravelEloquentStateMachines\Tests\TestModels\SalesManager;
use Asantibanez\LaravelEloquentStateMachines\Tests\TestModels\SalesOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;

class QueryScopesTest extends TestCase
{
use RefreshDatabase;
use WithFaker;

/** @test */
public function can_get_models_with_transition_responsible_model()
{
//Arrange
$salesManager = factory(SalesManager::class)->create();

$anotherSalesManager = factory(SalesManager::class)->create();

factory(SalesOrder::class)->create()->status()->transitionTo('approved', [], $salesManager);
factory(SalesOrder::class)->create()->status()->transitionTo('approved', [], $salesManager);
factory(SalesOrder::class)->create()->status()->transitionTo('approved', [], $anotherSalesManager);

//Act
$salesOrders = SalesOrder::with([])
->whereHasStatus(function ($query) use ($salesManager) {
$query->withResponsible($salesManager);
})
->get()
;

//Assert
$this->assertEquals(2, $salesOrders->count());

$salesOrders->each(function (SalesOrder $salesOrder) use ($salesManager) {
$this->assertEquals($salesManager->id, $salesOrder->status()->snapshotWhen('approved')->responsible->id);
});
}

/** @test */
public function can_get_models_with_transition_responsible_id()
{
//Arrange
$salesManager = factory(SalesManager::class)->create();

$anotherSalesManager = factory(SalesManager::class)->create();

factory(SalesOrder::class)->create()->status()->transitionTo('approved', [], $salesManager);
factory(SalesOrder::class)->create()->status()->transitionTo('approved', [], $anotherSalesManager);

//Act
$salesOrders = SalesOrder::with([])
->whereHasStatus(function ($query) use ($salesManager) {
$query->withResponsible($salesManager->id);
})
->get()
;

//Assert
$this->assertEquals(1, $salesOrders->count());
}

/** @test */
public function can_get_models_with_specific_transition()
{
//Arrange
$salesOrder = factory(SalesOrder::class)->create();
$salesOrder->status()->transitionTo('approved');
$salesOrder->status()->transitionTo('processed');

$anotherSalesOrder = factory(SalesOrder::class)->create();
$anotherSalesOrder->status()->transitionTo('approved');

//Act
$salesOrders = SalesOrder::with([])
->whereHasStatus(function ($query) {
$query->withTransition('approved', 'processed');
})
->get()
;

//Assert
$this->assertEquals(1, $salesOrders->count());

$this->assertEquals($salesOrder->id, $salesOrders->first()->id);
}

/** @test */
public function can_get_models_with_specific_transition_to_state()
{
//Arrange
$salesOrder = factory(SalesOrder::class)->create();
$salesOrder->status()->transitionTo('approved');
$salesOrder->status()->transitionTo('processed');

$anotherSalesOrder = factory(SalesOrder::class)->create();
$anotherSalesOrder->status()->transitionTo('approved');

//Act
$salesOrders = SalesOrder::with([])
->whereHasStatus(function ($query) {
$query->transitionedTo('processed');
})
->get()
;

//Assert
$this->assertEquals(1, $salesOrders->count());

$this->assertEquals($salesOrder->id, $salesOrders->first()->id);
}

/** @test */
public function can_get_models_with_specific_transition_from_state()
{
//Arrange
$salesOrder = factory(SalesOrder::class)->create();
$salesOrder->status()->transitionTo('approved');
$salesOrder->status()->transitionTo('processed');

$anotherSalesOrder = factory(SalesOrder::class)->create();
$anotherSalesOrder->status()->transitionTo('approved');

//Act
$salesOrders = SalesOrder::with([])
->whereHasStatus(function ($query) {
$query->transitionedFrom('approved');
})
->get()
;

//Assert
$this->assertEquals(1, $salesOrders->count());

$this->assertEquals($salesOrder->id, $salesOrders->first()->id);
}

/** @test */
public function can_get_models_with_specific_transition_custom_property()
{
//Arrange
$salesOrder = factory(SalesOrder::class)->create();
$salesOrder->status()->transitionTo('approved', ['comments' => 'Checked']);

$anotherSalesOrder = factory(SalesOrder::class)->create();
$anotherSalesOrder->status()->transitionTo('approved', ['comments' => 'Needs further revision']);

//Act
$salesOrders = SalesOrder::with([])
->whereHasStatus(function ($query) {
$query->withCustomProperty('comments', 'like', '%Check%');
})
->get()
;

//Assert
$this->assertEquals(1, $salesOrders->count());

$this->assertEquals($salesOrder->id, $salesOrders->first()->id);
}

/** @test */
public function can_get_models_using_multiple_state_machines_transitions()
{
//Arrange
$salesOrder = factory(SalesOrder::class)->create();
$salesOrder->status()->transitionTo('approved');
$salesOrder->status()->transitionTo('processed');

$anotherSalesOrder = factory(SalesOrder::class)->create();
$anotherSalesOrder->status()->transitionTo('approved');

//Act


$salesOrders = SalesOrder::with([])
->whereHasStatus(function ($query) {
$query->transitionedTo('approved');
})
->whereHasStatus(function ($query) {
$query->transitionedTo('processed');
})
->get()
;

//Assert
$this->assertEquals(1, $salesOrders->count());

$this->assertEquals($salesOrder->id, $salesOrders->first()->id);
}
}

0 comments on commit e0ecafb

Please sign in to comment.