Skip to content

Commit 6c81a2d

Browse files
authored
Merge pull request #144 from tighten/gc/adds-becoming
Adds `become` method
2 parents fd7dfc9 + a4e0da9 commit 6c81a2d

File tree

7 files changed

+296
-53
lines changed

7 files changed

+296
-53
lines changed

.editorconfig

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
indent_size = 4
7+
indent_style = space
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true
10+
11+
[*.md]
12+
trim_trailing_whitespace = false
13+
14+
[*.{yml,yaml}]
15+
indent_size = 2
16+
17+
[docker-compose.yml]
18+
indent_size = 4

.github/workflows/tests.yml

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@ jobs:
1111
strategy:
1212
fail-fast: true
1313
matrix:
14-
php: [8.0, 8.1, 8.2, 8.3]
15-
laravel: ['9.*', '10.*', '11.*', '12.*']
14+
php: [8.1, 8.2, 8.3]
15+
laravel: ["10.*", "11.*", "12.*"]
1616
dependency-version: [prefer-stable]
1717
include:
18-
- laravel: 9.*
19-
testbench: 7.*
2018
- laravel: 10.*
2119
testbench: 8.*
2220
- laravel: 11.*
@@ -26,14 +24,8 @@ jobs:
2624
exclude:
2725
- laravel: 9.*
2826
php: 8.3
29-
- laravel: 10.*
30-
php: 8.0
31-
- laravel: 11.*
32-
php: 8.0
3327
- laravel: 11.*
3428
php: 8.1
35-
- laravel: 12.*
36-
php: 8.0
3729
- laravel: 12.*
3830
php: 8.1
3931

composer.json

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,46 @@
11
{
2-
"name": "tightenco/parental",
3-
"description": "A simple eloquent trait that allows relationships to be accessed through child models.",
4-
"license": "MIT",
5-
"authors": [
6-
{
7-
"role": "author",
8-
"name": "Caleb Porzio",
9-
"email": "calebporzio@gmail.com",
10-
"homepage": "https://calebporzio.com/"
11-
},
12-
{
13-
"role": "developer",
14-
"name": "Steve McDougall",
15-
"email": "juststevemcd@gmail.com",
16-
"homepage": "https://www.juststeveking.uk/"
17-
}
18-
],
19-
"require": {
20-
"php": "^8.0",
21-
"illuminate/database": "^9.0||^10.0||^11.0||^12.0",
22-
"illuminate/events": "^9.0||^10.0||^11.0||^12.0"
2+
"name": "tightenco/parental",
3+
"description": "A simple eloquent trait that allows relationships to be accessed through child models.",
4+
"license": "MIT",
5+
"authors": [
6+
{
7+
"role": "author",
8+
"name": "Caleb Porzio",
9+
"email": "calebporzio@gmail.com",
10+
"homepage": "https://calebporzio.com/"
2311
},
24-
"require-dev": {
25-
"orchestra/testbench": "^7.0||^8.0||^9.0||^10.0",
26-
"phpunit/phpunit": "^9.5.10||^10.5||^11.5.3",
27-
"tightenco/duster": "^3.1"
28-
},
29-
"autoload": {
30-
"psr-4": {
31-
"Parental\\": "src/"
32-
}
33-
},
34-
"autoload-dev": {
35-
"psr-4": {
36-
"Parental\\Tests\\": "tests/",
37-
"Database\\Factories\\": "tests/factories/"
38-
}
39-
},
40-
"scripts": {
41-
"lint": "vendor/bin/duster lint",
42-
"fix": "vendor/bin/duster fix"
43-
},
44-
"minimum-stability": "dev",
45-
"prefer-stable": true
12+
{
13+
"role": "developer",
14+
"name": "Steve McDougall",
15+
"email": "juststevemcd@gmail.com",
16+
"homepage": "https://www.juststeveking.uk/"
17+
}
18+
],
19+
"require": {
20+
"php": "^8.1",
21+
"illuminate/database": "^10.0||^11.0||^12.0",
22+
"illuminate/events": "^10.0||^11.0||^12.0"
23+
},
24+
"require-dev": {
25+
"orchestra/testbench": "^8.0||^9.0||^10.0",
26+
"phpunit/phpunit": "^10.5||^11.5.3",
27+
"tightenco/duster": "^3.1"
28+
},
29+
"autoload": {
30+
"psr-4": {
31+
"Parental\\": "src/"
32+
}
33+
},
34+
"autoload-dev": {
35+
"psr-4": {
36+
"Parental\\Tests\\": "tests/",
37+
"Database\\Factories\\": "tests/factories/"
38+
}
39+
},
40+
"scripts": {
41+
"lint": "vendor/bin/duster lint",
42+
"fix": "vendor/bin/duster fix"
43+
},
44+
"minimum-stability": "dev",
45+
"prefer-stable": true
4646
}

readme.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,68 @@ class User extends Model
174174
}
175175
```
176176

177+
## Transforming Models Between Types
178+
179+
You may transform a model from one type to another using the `become()` method.
180+
181+
```php
182+
namespace App\Models;
183+
184+
use Illuminate\Database\Eloquent\Model;
185+
use Parental\HasChildren;
186+
use Parental\HasParent;
187+
188+
class Order extends Model
189+
{
190+
use HasChildren;
191+
192+
protected $fillable = ['type', 'total'];
193+
194+
protected $childTypes = [
195+
'pending' => PendingOrder::class,
196+
'shipped' => ShippedOrder::class,
197+
];
198+
}
199+
200+
class PendingOrder extends Order
201+
{
202+
use HasParent;
203+
}
204+
205+
class ShippedOrder extends Order
206+
{
207+
use HasParent;
208+
}
209+
```
210+
211+
```php
212+
use App\Models\Order;
213+
use App\Models\ShippedOrder;
214+
215+
// Retrieve a pending order
216+
$order = Order::first();
217+
218+
// Ship the order by transforming it
219+
$order = $order->become(ShippedOrder::class);
220+
221+
// Updates the "type" column to "shipped" and returns a ShippedOrder instance
222+
$order->save();
223+
```
224+
225+
### What problem did we just solve?
226+
227+
The `become()` method will return a new instance of the specified child model with all the attributes of the original model. You must call `save()` on the returned model to persist the change to the database. This allows you to easily transition a model between different types while maintaining its data integrity, such as changing an order from pending to shipped, or a draft post to a published post.
228+
229+
This is also useful when you're using observers or callbacks, since the specific child model's behavior will be triggered after the transition.
230+
231+
A new model event is fired when a model is _becoming_ another type, you may listen to it like so:
232+
233+
```php
234+
ShippedOrder::becoming(function ($shippedOrder) {
235+
// Do something before the model is saved...
236+
});
237+
```
238+
177239
## Laravel Nova Support
178240

179241
If you want to use share parent Nova resources with child models, you may register the following provider at the end of the boot method of your NovaServiceProvider:

src/HasChildren.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ trait HasChildren
2020
*/
2121
protected $hasChildren = true;
2222

23+
/**
24+
* Register a becoming model event with the dispatcher.
25+
*
26+
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
27+
*/
28+
public static function becoming($callback): void
29+
{
30+
static::registerModelEvent('becoming', $callback);
31+
}
32+
2333
/**
2434
* Register a model event with the dispatcher.
2535
*
@@ -253,6 +263,31 @@ public function getChildTypes(): array
253263
return [];
254264
}
255265

266+
/**
267+
* Convert the current model instance into another child type.
268+
*
269+
* @template T of object
270+
*
271+
* @param class-string<T> $class
272+
* @return new<T>
273+
*/
274+
public function become(string $class): object
275+
{
276+
return tap(new $class($attributes = $this->getAttributes()), function ($instance) use ($class, $attributes) {
277+
$instance->setRawAttributes(array_merge($attributes, [
278+
$this->getInheritanceColumn() => $this->classToAlias($class),
279+
]));
280+
281+
$instance->exists = true;
282+
283+
$instance->setConnection($this->getConnectionName());
284+
285+
$instance->setRelations($this->relations);
286+
287+
$instance->fireModelEvent('becoming', false);
288+
});
289+
}
290+
256291
/**
257292
* @return mixed
258293
*/
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
namespace Parental\Tests\Features;
4+
5+
use Illuminate\Support\Facades\Event;
6+
use Parental\Tests\Models\Car;
7+
use Parental\Tests\Models\ClawHammer;
8+
use Parental\Tests\Models\Mallet;
9+
use Parental\Tests\Models\SledgeHammer;
10+
use Parental\Tests\Models\Vehicle;
11+
use Parental\Tests\TestCase;
12+
13+
class ModelsCanBecomeOtherTypesTest extends TestCase
14+
{
15+
/** @test */
16+
public function child_model_can_become_another_child_type()
17+
{
18+
$car = Car::create(['driver_id' => 1]);
19+
20+
$vehicle = $car->become(Vehicle::class);
21+
22+
$this->assertEquals($car->id, $vehicle->id);
23+
$this->assertEquals($car->driver_id, $vehicle->driver_id);
24+
$this->assertEquals($car->created_at, $vehicle->created_at);
25+
$this->assertEquals($car->updated_at, $vehicle->updated_at);
26+
}
27+
28+
/** @test */
29+
public function become_marks_the_instance_as_existing()
30+
{
31+
$clawHammer = ClawHammer::create();
32+
33+
$sledgeHammer = $clawHammer->become(SledgeHammer::class);
34+
35+
$this->assertTrue($sledgeHammer->exists);
36+
}
37+
38+
/** @test */
39+
public function become_preserves_relationships()
40+
{
41+
$car = Car::create(['driver_id' => 1]);
42+
$car->load('driver');
43+
44+
$vehicle = $car->become(Vehicle::class);
45+
46+
$this->assertTrue($vehicle->relationLoaded('driver'));
47+
}
48+
49+
/** @test */
50+
public function become_preserves_connection()
51+
{
52+
$clawHammer = ClawHammer::create();
53+
$connectionName = $clawHammer->getConnectionName();
54+
55+
$sledgeHammer = $clawHammer->become(SledgeHammer::class);
56+
57+
$this->assertEquals($connectionName, $sledgeHammer->getConnectionName());
58+
}
59+
60+
/** @test */
61+
public function become_can_transform_parent_to_child()
62+
{
63+
$vehicle = Vehicle::create(['type' => 'truck']);
64+
65+
$this->assertInstanceOf(Vehicle::class, $vehicle);
66+
$this->assertEquals('truck', $vehicle->type);
67+
68+
$car = $vehicle->become(Car::class);
69+
70+
$this->assertInstanceOf(Car::class, $car);
71+
$this->assertEquals('car', $car->type);
72+
$this->assertEquals($vehicle->id, $car->id);
73+
}
74+
75+
/** @test */
76+
public function become_sets_the_correct_type_alias()
77+
{
78+
$clawHammer = ClawHammer::create();
79+
80+
$mallet = $clawHammer->become(Mallet::class);
81+
82+
$this->assertEquals('mallet', $mallet->type);
83+
}
84+
85+
/** @test */
86+
public function come_calls_model_events_on_specified_model(): void
87+
{
88+
$carEventCalled = false;
89+
90+
Car::saved(function () use (&$carEventCalled) {
91+
$carEventCalled = true;
92+
});
93+
94+
$vehicle = Vehicle::create(['type' => 'truck']);
95+
96+
$this->assertFalse($carEventCalled);
97+
98+
$car = $vehicle->become(Car::class);
99+
$car->save();
100+
101+
$this->assertInstanceOf(Car::class, $car);
102+
$this->assertEquals('car', $car->type);
103+
$this->assertEquals($vehicle->id, $car->id);
104+
105+
$this->assertTrue($carEventCalled);
106+
}
107+
108+
/** @test */
109+
public function fires_becoming_model_event(): void
110+
{
111+
$becomingEventCalledCount = 0;
112+
113+
Event::listen('eloquent.becoming: ' . Car::class, function () use (&$becomingEventCalledCount) {
114+
$becomingEventCalledCount++;
115+
});
116+
117+
Car::becoming(function () use (&$becomingEventCalledCount) {
118+
$becomingEventCalledCount++;
119+
});
120+
121+
$vehicle = Vehicle::create(['type' => 'truck']);
122+
123+
$this->assertEquals(0, $becomingEventCalledCount);
124+
125+
$car = $vehicle->become(Car::class);
126+
$car->save();
127+
128+
$this->assertInstanceOf(Car::class, $car);
129+
$this->assertEquals('car', $car->type);
130+
$this->assertEquals($vehicle->id, $car->id);
131+
132+
$this->assertEquals(2, $becomingEventCalledCount);
133+
}
134+
}

0 commit comments

Comments
 (0)