Skip to content

Commit

Permalink
Merge pull request #669 from spatie/data-for-livewire
Browse files Browse the repository at this point in the history
laravel-data & Livewire
  • Loading branch information
rubenvanassche authored Mar 1, 2024
2 parents 239ed26 + ad4ad2e commit 605626a
Show file tree
Hide file tree
Showing 23 changed files with 766 additions and 10 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
"fakerphp/faker": "^1.14",
"friendsofphp/php-cs-fixer": "^3.0",
"inertiajs/inertia-laravel": "dev-master#4508fd1",
"livewire/livewire": "^3.0",
"mockery/mockery": "^1.6",
"nesbot/carbon": "^2.63",
"nette/php-generator": "^3.5",
"nunomaduro/larastan": "^2.0",
"orchestra/testbench": "^8.0|^9.0",
"pestphp/pest": "^2.31",
"pestphp/pest-plugin-laravel": "^2.0",
"pestphp/pest-plugin-livewire": "^2.1",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.1",
"phpunit/phpunit": "^10.0",
Expand Down
16 changes: 12 additions & 4 deletions config/data.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<?php

use Illuminate\Support\Collection;
use Illuminate\Support\Enumerable;

return [
Expand Down Expand Up @@ -111,25 +110,34 @@
* the data classes. You can override these settings by passing options to the command.
*/
'commands' => [
/*
/**
* Provides default configuration for the `make:data` command. These settings can be overridden with options
* passed directly to the `make:data` command for generating single Data classes, or if not set they will
* automatically fall back to these defaults. See `php artisan make:data --help` for more information
*/
'make' => [
/*
/**
* The default namespace for generated Data classes. This exists under the application's root namespace,
* so the default 'Data` will end up as '\App\Data', and generated Data classes will be placed in the
* app/Data/ folder. Data classes can live anywhere, but this is where `make:data` will put them.
*/
'namespace' => 'Data',

/*
/**
* This suffix will be appended to all data classes generated by make:data, so that they are less likely
* to conflict with other related classes, controllers or models with a similar name without resorting
* to adding an alias for the Data object. Set to a blank string (not null) to disable.
*/
'suffix' => 'Data',
],
],

/**
* When using Livewire, the package allows you to enable or disable the synths
* these synths will automatically handle the data objects and their
* properties when used in a Livewire component.
*/
'livewire' => [
'enable_synths' => false,
],
];
83 changes: 82 additions & 1 deletion docs/advanced-usage/use-with-livewire.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ title: Use with Livewire
weight: 10
---

> Livewire is a full-stack framework for Laravel that makes building dynamic interfaces simple without leaving the comfort of Laravel.
> Livewire is a full-stack framework for Laravel that makes building dynamic interfaces simple without leaving the
> comfort of Laravel.
Laravel Data works excellently with [Laravel Livewire](https://laravel-livewire.com).

Expand Down Expand Up @@ -46,3 +47,83 @@ class SongData extends Data implements Wireable
}
}
```

## Livewire Synths (Experimental)

Laravel Data also provides a way to use Livewire Synths with your data objects. It will allow you to use data objects
and collections
without the need to make them Wireable. This is an experimental feature and is subject to change.

You can enable this feature by setting the config option in `data.php`:

```php
'livewire' => [
'enable_synths' => false,
]
```

Once enabled, you can use data objects within your Livewire components without the need to make them Wireable:

```php
class SongUpdateComponent extends Component
{
public SongData $data;

public function mount(public int $id): void
{
$this->data = SongData::from(Song::findOrFail($id));
}

public function save(): void
{
Artist::findOrFail($this->id)->update($this->data->toArray());
}

public function render(): string
{
return <<<'BLADE'
<div>
<h1>Songs</h1>
<input type="text" wire:model.live="data.title">
<input type="text" wire:model.live="data.artist">
<p>Title: {{ $data->title }}</p>
<p>Artist: {{ $data->artist }}</p>
<button wire:click="save">Save</button>
</div>
BLADE;
}
}
```

### Lazy

It is possible to use Lazy properties, these properties will not be sent over the wire unless they're included. **Always
include properties permanently** because a data object is being transformed and then cast again between Livewire
requests the includes should be permanent.

It is possible to query lazy nested data objects, it is however not possible to query lazy properties which are not a data:

```php
use Spatie\LaravelData\Lazy;class LazySongData extends Data
{
public function __construct(
public Lazy|ArristData $artist,
public Lazy|string $title,
) {}
}
```

Within your Livewire view

```php
$this->data->artist->name; // Works
$this->data->title; // Does not work
```

### Validation

Laravel data **does not provide validation** when using Livewire, you should do this yourself! This is because laravel-data
does not support object validation at the moment. Only validating payloads which eventually become data objects.
The validation could technically happen when hydrating the data object, but this is not implemented
because we cannot guarantee that every hydration happens when a user made sure the data is valid
and thus the payload should be validated.
8 changes: 8 additions & 0 deletions src/Concerns/ContextableData.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,12 @@ public function getDataContext(): DataContext

return $this->_dataContext;
}

public function setDataContext(
DataContext $dataContext
): static {
$this->_dataContext = $dataContext;

return $this;
}
}
13 changes: 13 additions & 0 deletions src/LaravelDataServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

namespace Spatie\LaravelData;

use Livewire\Livewire;
use Spatie\LaravelData\Commands\DataMakeCommand;
use Spatie\LaravelData\Commands\DataStructuresCacheCommand;
use Spatie\LaravelData\Contracts\BaseData;
use Spatie\LaravelData\Support\Caching\DataStructureCache;
use Spatie\LaravelData\Support\DataConfig;
use Spatie\LaravelData\Support\Livewire\LivewireDataCollectionSynth;
use Spatie\LaravelData\Support\Livewire\LivewireDataSynth;
use Spatie\LaravelData\Support\VarDumper\VarDumperManager;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
Expand Down Expand Up @@ -50,6 +53,16 @@ function () {
fn ($container) => $class::from($container['request'])
);
});

if(config('data.livewire.enable_synths') && class_exists(Livewire::class)) {
$this->registerLivewireSynths();
}
}

protected function registerLivewireSynths(): void
{
Livewire::propertySynthesizer(LivewireDataSynth::class);
Livewire::propertySynthesizer(LivewireDataCollectionSynth::class);
}

public function packageBooted(): void
Expand Down
3 changes: 3 additions & 0 deletions src/Support/DataClassMorphMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public function merge(array $map): self
return $this;
}

/**
* @return class-string<BaseData>|null
*/
public function getMorphedDataClass(string $alias): ?string
{
return $this->map[$alias] ?? null;
Expand Down
20 changes: 20 additions & 0 deletions src/Support/Lazy/LivewireLostLazy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Spatie\LaravelData\Support\Lazy;

use Exception;
use Spatie\LaravelData\Lazy;

class LivewireLostLazy extends Lazy
{
public function __construct(
public string $dataClass,
public string $propertyName
) {
}

public function resolve(): mixed
{
return throw new Exception("Lazy property `{$this->dataClass}::{$this->propertyName}` was lost when the data object was transformed to be used by Livewire. You can include the property and then the correct value will be set when creating the data object from Livewire again.");
}
}
82 changes: 82 additions & 0 deletions src/Support/Livewire/LivewireDataCollectionSynth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace Spatie\LaravelData\Support\Livewire;

use Livewire\Mechanisms\HandleComponents\ComponentContext;
use Livewire\Mechanisms\HandleComponents\Synthesizers\Synth;
use Spatie\LaravelData\Contracts\BaseData;
use Spatie\LaravelData\DataCollection;
use Spatie\LaravelData\Support\DataConfig;

class LivewireDataCollectionSynth extends Synth
{
protected DataConfig $dataConfig;

public static string $key = 'ldco';

public function __construct(ComponentContext $context, $path)
{
$this->dataConfig = app(DataConfig::class);

parent::__construct($context, $path);
}

public static function match($target): bool
{
return is_a($target, DataCollection::class, true);
}

public function get(&$target, $key): BaseData
{
return $target[$key];
}

public function set(&$target, $key, $value)
{
$target[$key] = $value;
}

/**
* @param callable(array-key, mixed):mixed $dehydrateChild
*/
public function dehydrate(DataCollection $target, callable $dehydrateChild): array
{
$morph = $this->dataConfig->morphMap->getDataClassAlias($target->dataClass) ?? $target->dataClass;

$payload = [];

foreach ($target->toCollection() as $key => $child) {
$payload[$key] = $dehydrateChild($key, $child);
}

return [
$payload,
[
'dataCollectionClass' => $target::class,
'dataMorph' => $morph,
'context' => encrypt($target->getDataContext()),
],
];
}

/**
* @param callable(array-key, mixed):mixed $hydrateChild
*/
public function hydrate($value, $meta, $hydrateChild)
{
$context = decrypt($meta['context']);
$dataCollectionClass = $meta['dataCollectionClass'];
$dataClass = $this->dataConfig->morphMap->getMorphedDataClass($meta['dataMorph']) ?? $meta['dataMorph'];

foreach ($value as $key => $child) {
$value[$key] = $hydrateChild($key, $child);
}

/** @var DataCollection $dataCollection */
$dataCollection = new $dataCollectionClass($dataClass, $value);

$dataCollection->setDataContext($context);

return $dataCollection;
}
}
Loading

0 comments on commit 605626a

Please sign in to comment.