Skip to content

Commit

Permalink
Model extending (#1661)
Browse files Browse the repository at this point in the history
This PR looks to build on the current work done for model extending and
includes but not limited to the following changes:


- Filament resources use the model contract for DI
- Change to use Laravel's `morphMap` instead of having concrete classes
in the database for polymorphic relationships
- There was an issue with custom models when they extended Lunar ones,
the `newFactory` method didn't like having the return type.
- Added a `addDir`
- Tweaked `guessContractClass` to take namespace into account as not all
Lunar models (from addons) will have the same namespace
- Added `addDirectory` method to prevent excessive calls to `replace` or
`add` methods.
- Updated relationships to use `modelClass()`
- Static functions should forward to the custom class when called.
- If a custom class name is different to the one it extends i.e.
`App\Models\MyProduct` instead of `App\Models\Product` it will still use
the correct table name.
- Tweaked `observe` method on models that use `HasModelExtending` to
forward to the custom class even if observe is called on the Lunar
model.
- Removed redundant tests and updated docs.

Set as draft for the moment as there is a fair amount of changes so just
want to double check before hitting the button.

---------

Co-authored-by: Glenn Jacobs <[email protected]>
Co-authored-by: glennjacobs <[email protected]>
Co-authored-by: wychoong <[email protected]>
Co-authored-by: alecritson <[email protected]>
Co-authored-by: mattdfloyd <[email protected]>
  • Loading branch information
6 people authored Aug 28, 2024
1 parent fc792b0 commit 3696422
Show file tree
Hide file tree
Showing 265 changed files with 2,612 additions and 1,654 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"spatie/laravel-blink": "^1.7",
"spatie/laravel-medialibrary": "^11.0.0",
"spatie/laravel-permission": "^6.4",
"spatie/php-structure-discoverer": "^2.0",
"stripe/stripe-php": "^14.4",
"technikermathe/blade-lucide-icons": "^v3.0"
},
Expand Down
255 changes: 66 additions & 189 deletions docs/core/extending/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,238 +8,130 @@ Lunar provides a number of Eloquent Models and quite often in custom application
We highly suggest using your own Eloquent Models to add additional data, rather than trying to change fields on the core Lunar models.
:::

## Extendable Models
All Lunar models are now extendable.
This means you can now add your own functionality or change out existing core model behaviour using your own model implementations.
## Replaceable Models
All Lunar models are replaceable, this means you can instruct Lunar to use your own custom model, throughout the ecosystem, using dependency injection.

### Registration:

### Registration
We recommend registering your own models for your application within the boot method of your Service Provider.
When registering your models, you will need to set the Lunar core model as the key and then your own model implementation as the value.

Here is an example below where we are extending 10 core models from your main AppServiceProvider:
When registering your models, you will need to set the Lunar model's contract as the first argument then your own model implementation for the second.

```php
use Lunar\Models\Product;
use Lunar\Models\ProductVariant;
use Lunar\Models\ProductOption;
use Lunar\Models\ProductOptionValue;
use Lunar\Models\Collection;
use Lunar\Models\Customer;
use Lunar\Models\Cart;
use Lunar\Models\CartLine;
use Lunar\Models\Order;
use Lunar\Models\OrderLine;

```php
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
$models = collect([
Product::class => \App\Models\Product::class,
ProductVariant::class => \App\Models\ProductVariant::class,
ProductOption::class => \App\Models\ProductOption::class,
ProductOptionValue::class => \App\Models\ProductOptionValue::class,
Collection::class => \App\Models\Collection::class,
Customer::class => \App\Models\Customer::class,
Cart::class => \App\Models\Cart::class,
CartLine::class => \App\Models\CartLine::class,
Order::class => \App\Models\Order::class,
OrderLine::class => \App\Models\OrderLine::class,
]);

ModelManifest::register($models);
\Lunar\Facades\ModelManifest::replace(
\Lunar\Models\Contracts\Product::class,
\App\Model\Product::class,
);
}
```

### Swap Implementation
You can override the model implementation at any time by calling the swap method on the core model.
When you call the swap method this will update the key value pair for the registered model. If you need to go back to the previous implementation then simply call the swap method again passing through your registered implementation.
#### Registering multiple Lunar models.

```php
namespace App\Models;

use Lunar\Models\ProductVariant;
If you have multiple models you want to replace, instead of manually replacing them one by one, you can specify a directory for Lunar to look in for Lunar models to use.
This assumes that each model extends its counterpart model i.e. `App\Models\Product` extends `Lunar\Models\Product`.

class ProductSwapModel extends \Lunar\Models\Product
```php
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
/**
* This will return the default variant for the product.
*
* @return bool
*/
public function defaultVariant(): ProductVariant
{
return $this->variants->first();
}
\Lunar\Facades\ModelManifest::addDirectory(
__DIR__.'/../Models'
);
}
```

```php
$product = \Lunar\Models\Product::find(1);

// This will swap out the registered implementation.
$product->swap(\App\Models\ProductSwapModel::class);
### Route binding

// You can now call this new method
$default = $product->defaultVariant();
Route binding is supported for your own routes and simply requires the relevant contract class to be injected.

// Swap again to go back to your original implementation or perhaps define a new one.
$product->swap(\App\Models\Product::class);
```php
Route::get('products/{id}', function (\Lunar\Models\Contracts\Product $product) {
$product; // App\Models\Product
});
```

### Examples
Here are some example simple use cases of extending the core models.
You are required to extend the core model `Lunar\Models\[Model]` in order for the relationships to function correctly.
### Relationship support

#### Example 1 - Adding static method (ProductOption Model)
If you replace a model which is used in a relationship, you can easily get your own model back via relationship methods. Assuming we want to use our own instance of `App\Models\ProductVariant`.

```php
namespace App\Models;

use Illuminate\Support\Collection;

class ProductOption extends \Lunar\Models\ProductOption
// In our service provider.
public function boot()
{
public static function getSizes(): Collection
{
return static::whereHandle('size')->first()->values;
}
\Lunar\Facades\ModelManifest::replace(
\Lunar\Models\Contracts\ProductVariant::class,
\App\Model\ProductVariant::class,
);
}
```
In this example you can access the static method via `\Lunar\Models\ProductOption::getSizes()`
Note: Static methods will not allow you to jump to the function declaration.
As a workaround simply add @see inline docblock:

```php
`/** @see \App\Models\ProductOption::getSizesStatic() */`
$newStaticMethod = \Lunar\Models\ProductOption::getSizesStatic();
```

#### Example 2 - Overriding trait method (Product Model)
// Somewhere else in your code...

```php
namespace App\Models;
$product = \Lunar\Models\Product::first();
$product->variants->first(); // App\Models\ProductVariant
```

use App\Concerns\SearchableTrait;
### Static call forwarding

class Product extends \Lunar\Models\Product
{
use SearchableTrait;
}
```
Note: shouldBeSearchable could also be overridden by adding directly to the Product class above.
In this example we are showing you how the core model can be made aware of your own model and trait methods.
If you have custom methods in your own model, you can call those functions directly from the Lunar model instance.

What this also means now the core model can forward call to your extended methods.
Scout in this case will also be made aware that shouldBeSearchable will return false.
Assuming we want to provide a new function to a product variant model.

```php
namespace App\Concerns;
<?php

trait SearchableTrait
namespace App\Models;

class ProductVariant extends \Lunar\Models\ProductVariant
{
/**
* Determine if the model should be searchable.
* @see \Laravel\Scout\Searchable::shouldBeSearchable()
*
* @return bool
*/
public function shouldBeSearchable()
public function someCustomMethod()
{
return false;
return 'Hello!';
}
}
```
#### Example 3 - Overriding cart address functionality (Cart Model)

```php
namespace App\Models;

use App\Concerns\HasAddresses;
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
use Illuminate\Database\Eloquent\Casts\AsCollection;

/**
* Class Cart
*
* @property \Illuminate\Support\Collection $billingAddress
* @property \Illuminate\Support\Collection $shippingAddress
*
*/
class Cart extends \Lunar\Models\Cart
// In your service provider.
public function boot()
{
use HasAddresses;

/**
* {@inheritDoc}
*/
protected $casts = [
'completed_at' => 'datetime',
'meta' => AsArrayObject::class,
'shipping_data' => AsCollection::class,
];
\Lunar\Facades\ModelManifest::replace(
\Lunar\Models\Contracts\ProductVariant::class,
\App\Model\ProductVariant::class,
);
}
```
Note: You can override the casts in a model for example useful when adding new json fields.
In this example we are setting shipping_data cast to store as json. (You will of course need to create your migration)

The trait below demonstrates how to fully extend the cart model functionality.
Somewhere else in your app...

```php
namespace App\Concerns;

trait HasAddresses
{
/**
* Return the address relationships.
*
* @return \Illuminate\Support\Collection
*/
public function addresses(): Collection
{
return ! $this->isSameAddress()
? $this->billingAddress->merge($this->shippingAddress)
: $this->billingAddress;
}
\Lunar\Models\ProductVariant::someCustomMethod(); // Hello!
\App\Models\ProductVariant::someCustomMethod(); // Hello!
```

/**
* Return the shipping address relationship.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function shippingAddress()
{
return $this->belongsTo(Address::class, 'shipping_address_id');
}
### Observers

/**
* Return the billing address relationship.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function billingAddress()
{
return $this->belongsTo(Address::class, 'billing_address_id');
}
If you have observers in your app which call `observe` on the Lunar model, these will still work as intended when you replace any of the models, this means if you
want to add your own custom observers, you can just reference the Lunar model and everything will be forwarded to the appropriate class.

/**
* Compare the shipping and billing address to see if they are the same.
*
* @return bool
*/
protected function isSameAddress(): bool
{
return $this->billingAddress->first()->id === $this->shippingAddress->first()->id;
}
}
```php
\Lunar\Models\Product::observe(/** .. */);
```

## Dynamic Eloquent Relationships

Eloquent relationships can be dynamically specified in code, allowing you to add additional relationships to the Lunar Models.
If you don't need to completely override or extend the Lunar models using the techniques above, you are still free to resolve relationships dynamically as Laravel provides out the box.

e.g.

Expand All @@ -253,18 +145,3 @@ Order::resolveRelationUsing('ticket', function ($orderModel) {
```

See [https://laravel.com/docs/eloquent-relationships#dynamic-relationships](https://laravel.com/docs/eloquent-relationships#dynamic-relationships) for more information.


## Macroable

All Lunar models have been made macroable. This is a Laravel technique to allow a developer to dynamically add methods to an existing class. This is ideal for adding helpful functions for your application.

Here is an example...

```php
use Lunar\Models\Product;

Product::macro('isDraft', function () {
return $this->status === 'draft';
});
```
26 changes: 13 additions & 13 deletions docs/core/reference/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ For example, a television might have the following attributes assigned...
Lunar\Models\Attribute
```

|Field|Description|
|:-|:-|
|`attribute_type`|Model type that can use attribute, e.g. `Lunar\Models\ProductType`|
|`attribute_group_id`|The associated group|
|`position`|An integer used to define the sorting order of attributes within attribute groups|
|`name`|Laravel Collection of translations `{'en': 'Screen Size'}`|
|`handle`|Kebab-cased reference, e.g. `screen-size`|
|`section`|An optional name to define where an attribute should be used.|
|`type`|The field type to be used, e.g. `Lunar\FieldTypes\Number`|
|`required`|Boolean|
|`default_value`||
|`configuration`|Meta data stored as a Laravel Collection|
|`system`|If set to true, indicates it should not be deleted|
|Field| Description |
|:-|:----------------------------------------------------------------------------------|
|`attribute_type`| Morph map of the model type that can use attribute, e.g. `product` |
|`attribute_group_id`| The associated group |
|`position`| An integer used to define the sorting order of attributes within attribute groups |
|`name`| Laravel Collection of translations `{'en': 'Screen Size'}` |
|`handle`| Kebab-cased reference, e.g. `screen-size` |
|`section`| An optional name to define where an attribute should be used. |
|`type`| The field type to be used, e.g. `Lunar\FieldTypes\Number` |
|`required`| Boolean |
|`default_value`| |
|`configuration`| Meta data stored as a Laravel Collection |
|`system`| If set to true, indicates it should not be deleted |

### Field Types

Expand Down
7 changes: 4 additions & 3 deletions docs/core/reference/carts.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,19 @@ Lunar\Models\CartLine
|:-----------------|:---------------------------------------------|
| id | |
| cart_id | |
| purchasable_type | e.g. `Lunar\Models\ProductVariant`. |
| purchasable_type | e.g. `product_variant` |
| purchasable_id | |
| quantity | |
| created_at | |
| updated_at | |
| meta | JSON data for saving any custom information. |

```php
$purchasable = \Lunar\Models\ProductVariant::create([/** ... */]);
$cartLine = new \Lunar\Models\CartLine([
'cart_id' => 1,
'purchasable_type' => ProductVariant::class,
'purchasable_id' => 123,
'purchasable_type' => $purchasable->getMorphClass(),
'purchasable_id' => $purchasable->id,
'quantity' => 2,
'meta' => [
'personalization' => 'Love you mum xxx',
Expand Down
2 changes: 1 addition & 1 deletion docs/core/reference/discounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Lunar\Models\DiscountPurchasable
|:-------------------|:------------------------|:------------------------------|
| `id` | | |
| `discount_id` | | |
| `purchasable_type` | | `Lunar\Models\ProductVariant`
| `purchasable_type` | | `product_variant`
| `type` | `condition` or `reward` |
| `created_at` | | |
| `updated_at` | | |
Expand Down
Loading

0 comments on commit 3696422

Please sign in to comment.