Skip to content

Commit

Permalink
Merge pull request #622 from spatie/structure-discoverer
Browse files Browse the repository at this point in the history
Structure caching
  • Loading branch information
rubenvanassche authored Dec 21, 2023
2 parents 0d6c883 + 9046387 commit e9cb661
Show file tree
Hide file tree
Showing 20 changed files with 456 additions and 85 deletions.
44 changes: 23 additions & 21 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,31 @@
}
],
"require" : {
"php" : "^8.1",
"illuminate/contracts" : "^9.30|^10.0",
"phpdocumentor/type-resolver" : "^1.5",
"spatie/laravel-package-tools" : "^1.9.0"
"php": "^8.1",
"illuminate/contracts": "^9.30|^10.0",
"phpdocumentor/type-resolver": "^1.5",
"spatie/laravel-package-tools": "^1.9.0",
"spatie/php-structure-discoverer": "^2.0"
},
"require-dev" : {
"fakerphp/faker" : "^1.14",
"friendsofphp/php-cs-fixer" : "^3.0",
"inertiajs/inertia-laravel" : "^0.6.3",
"nesbot/carbon" : "^2.63",
"nette/php-generator" : "^3.5",
"nunomaduro/larastan" : "^2.0",
"orchestra/testbench" : "^7.6|^8.0",
"pestphp/pest" : "^1.22",
"pestphp/pest-plugin-laravel" : "^1.3",
"phpbench/phpbench" : "^1.2",
"phpstan/extension-installer" : "^1.1",
"phpunit/phpunit" : "^9.3",
"spatie/invade" : "^1.0",
"spatie/laravel-typescript-transformer" : "^2.1.6",
"spatie/pest-plugin-snapshots" : "^1.1",
"spatie/phpunit-snapshot-assertions" : "^4.2",
"spatie/test-time" : "^1.2"
"fakerphp/faker": "^1.14",
"friendsofphp/php-cs-fixer": "^3.0",
"inertiajs/inertia-laravel": "^0.6.3",
"mockery/mockery": "^1.6",
"nesbot/carbon": "^2.63",
"nette/php-generator": "^3.5",
"nunomaduro/larastan": "^2.0",
"orchestra/testbench": "^7.6|^8.0",
"pestphp/pest": "^1.22",
"pestphp/pest-plugin-laravel": "^1.3",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.1",
"phpunit/phpunit": "^9.3",
"spatie/invade": "^1.0",
"spatie/laravel-typescript-transformer": "^2.1.6",
"spatie/pest-plugin-snapshots": "^1.1",
"spatie/phpunit-snapshot-assertions": "^4.2",
"spatie/test-time": "^1.2"
},
"autoload" : {
"psr-4" : {
Expand Down
29 changes: 24 additions & 5 deletions config/data.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<?php

return [
/*
/**
* The package will use this format when working with dates. If this option
* is an array, it will try to convert from the first format that works,
* and will serialize dates using the first format from the array.
*/
'date_format' => DATE_ATOM,

/*
/**
* Global transformers will take complex types and transform them into simple
* types.
*/
Expand All @@ -18,7 +18,7 @@
BackedEnum::class => Spatie\LaravelData\Transformers\EnumTransformer::class,
],

/*
/**
* Global casts will cast values into complex types when creating a data
* object from simple types.
*/
Expand All @@ -27,7 +27,7 @@
BackedEnum::class => Spatie\LaravelData\Casts\EnumCast::class,
],

/*
/**
* Rule inferrers can be configured here. They will automatically add
* validation rules to properties of a data object based upon
* the type of the property.
Expand All @@ -54,7 +54,7 @@
Spatie\LaravelData\Normalizers\JsonNormalizer::class,
],

/*
/**
* Data objects can be wrapped into a key like 'data' when used as a resource,
* this key can be set globally here for all data objects. You can pass in
* `null` if you want to disable wrapping.
Expand All @@ -68,4 +68,23 @@
* which will only enable the caster locally.
*/
'var_dumper_caster_mode' => 'development',

/**
* It is possible to skip the PHP reflection analysis of data objects
* when running in production. This will speed up the package. You
* can configure where data objects are stored and which cache
* store should be used.
*/
'structure_caching' => [
'directories' => [app_path('Data')],
'cache' => [
'store' => env('CACHE_DRIVER', 'file'),
'prefix' => 'laravel-data',
],
'reflection_discovery' => [
'enabled' => true,
'base_path' => base_path(),
'root_namespace' => null,
],
],
];
2 changes: 1 addition & 1 deletion docs/advanced-usage/commands.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Commands
weight: 15
weight: 16
---

## make:data
Expand Down
65 changes: 65 additions & 0 deletions docs/advanced-usage/performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
title: Performance
weight: 15
---

Laravel Data is a powerful package that leverages PHP reflection to infer as much information as possible. While this approach provides a lot of benefits, it does come with a minor performance overhead. This overhead is typically negligible during development, but it can become noticeable in a production environment with a large number of data objects.

Fortunately, Laravel Data is designed to operate efficiently without relying on reflection. It achieves this by allowing you to cache the results of its complex analysis. This means that the performance cost is incurred only once, rather than on every request. By caching the analysis results before deploying your application to production, you ensure that a pre-analyzed, cached version of the data objects is used, significantly improving performance.

## Caching

Laravel Data provides a command to cache the analysis results of your data objects. This command will analyze all of your data objects and store the results in a Laravel cache of your choice:

```
php artisan data:cache-structures
```

That's it, the command will search for all the data objects in your application and cache the analysis results. Be sure to always run this command after creating or modifying a data object or when deploying your application to production.

## Configuration

The caching mechanism can be configured in the `data.php` config file. By default, the cache store is set to the default cache store of your application. You can change this to any other cache driver supported by Laravel. A prefix can also be set for the cache keys stored:

```php
'structure_caching' => [
'cache' => [
'store' => 'redis',
'prefix' => 'laravel-data',
],
],
```

To find the data classes within your application, we're using the [php-structure-discoverer](https://github.com/spatie/php-structure-discoverer) package. This package allows you to configure the directories that will be searched for data objects. By default, the `app/data` directory is searched recursively. You can change this to any other directory or directories:

```php
'structure_caching' => [
'directories' => [
'app',
],
],
```

Structure discoverer uses reflection (enabled by default) or a PHP parser to find the data objects. You can disable the reflection based discovery and thus use the PHP parser discovery as such:

```php
'structure_caching' => [
'reflection_discovery' => [
'enabled' => false,
],
],
```

When using reflection discovery, the base directory and root namespace can be configured as such if you're using a non-standard directory structure or namespace

```php
'structure_caching' => [
'reflection_discovery' => [
'enabled' => true,
'base_path' => base_path(),
'root_namespace' => null,
],
],
```

You can read more about reflection discovery [here](https://github.com/spatie/php-structure-discoverer#parsers).
3 changes: 3 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@
<logging>
<junit outputFile="build/report.junit.xml"/>
</logging>
<php>
<env name="CACHE_DRIVER" value="array"/>
</php>
</phpunit>
53 changes: 53 additions & 0 deletions src/Commands/DataStructuresCacheCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Spatie\LaravelData\Commands;

use Illuminate\Console\Command;
use ReflectionClass;
use Spatie\LaravelData\Support\Caching\CachedDataConfig;
use Spatie\LaravelData\Support\Caching\DataClassFinder;
use Spatie\LaravelData\Support\Caching\DataStructureCache;
use Spatie\LaravelData\Support\DataClass;
use Spatie\LaravelData\Support\DataConfig;

class DataStructuresCacheCommand extends Command
{
protected $signature = 'data:cache-structures {--show-classes : Show the data classes cached}';

protected $description = 'Cache the internal data structures';

public function handle(
DataStructureCache $dataStructureCache,
DataConfig $dataConfig
): void {
$this->components->info('Caching data structures...');

$dataClasses = DataClassFinder::fromConfig(config('data.structure_caching'))->classes();

$cachedDataConfig = CachedDataConfig::initialize($dataConfig);

$dataStructureCache->storeConfig($cachedDataConfig);

$progressBar = $this->output->createProgressBar(count($dataClasses));

foreach ($dataClasses as $dataClass) {
$dataStructureCache->storeDataClass(
DataClass::create(new ReflectionClass($dataClass))
);

$progressBar->advance();
}

$progressBar->finish();

$this->line(PHP_EOL);
$this->line('Cached '.count($dataClasses).' data classes');

if ($this->option('show-classes')) {
$this->table(
['Data Class'],
array_map(fn (string $dataClass) => [$dataClass], $dataClasses)
);
}
}
}
14 changes: 11 additions & 3 deletions src/LaravelDataServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace Spatie\LaravelData;

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\VarDumper\VarDumperManager;
use Spatie\LaravelPackageTools\Package;
Expand All @@ -16,14 +18,20 @@ public function configurePackage(Package $package): void
$package
->name('laravel-data')
->hasCommand(DataMakeCommand::class)
->hasCommand(DataStructuresCacheCommand::class)
->hasConfigFile('data');
}

public function packageRegistered()
public function packageRegistered(): void
{
$this->app->singleton(
DataStructureCache::class,
fn () => new DataStructureCache(config('data.structure_caching.cache'))
);

$this->app->singleton(
DataConfig::class,
fn () => new DataConfig(config('data'))
fn () => $this->app->make(DataStructureCache::class)->getConfig() ?? new DataConfig(config('data'))
);

/** @psalm-suppress UndefinedInterfaceMethod */
Expand All @@ -39,7 +47,7 @@ public function packageRegistered()
});
}

public function packageBooted()
public function packageBooted(): void
{
$enableVarDumperCaster = match (config('data.var_dumper_caster_mode')) {
'enabled' => true,
Expand Down
10 changes: 4 additions & 6 deletions src/Resolvers/PartialsTreeFromRequestResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,21 @@ public function execute(

$dataClass = $this->dataConfig->getDataClass($dataClass);

$mapping = $dataClass->outputNameMapping->resolve();

$requestedIncludesTree = $this->partialsParser->execute(
$request->has('include') ? $this->arrayFromRequest($request, 'include') : [],
$mapping
$dataClass->outputNameMapping
);
$requestedExcludesTree = $this->partialsParser->execute(
$request->has('exclude') ? $this->arrayFromRequest($request, 'exclude') : [],
$mapping
$dataClass->outputNameMapping
);
$requestedOnlyTree = $this->partialsParser->execute(
$request->has('only') ? $this->arrayFromRequest($request, 'only') : [],
$mapping
$dataClass->outputNameMapping
);
$requestedExceptTree = $this->partialsParser->execute(
$request->has('except') ? $this->arrayFromRequest($request, 'except') : [],
$mapping
$dataClass->outputNameMapping
);

$allowedRequestIncludesTree = $this->allowedPartialsParser->execute('allowedRequestIncludes', $dataClass);
Expand Down
49 changes: 49 additions & 0 deletions src/Support/Caching/CachedDataConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Spatie\LaravelData\Support\Caching;

use Spatie\LaravelData\Support\DataClass;
use Spatie\LaravelData\Support\DataConfig;

class CachedDataConfig extends DataConfig
{
protected ?DataStructureCache $cache = null;

public function __construct()
{
parent::__construct([
'rule_inferrers' => [],
'transformers' => [],
'casts' => [],
]); // Ensure the parent object is constructed empty, todo v4: remove this and use a better constructor with factory
}

public function getDataClass(string $class): DataClass
{
return $this->cache?->getDataClass($class) ?? parent::getDataClass($class);
}

public function setCache(DataStructureCache $cache): self
{
$this->cache = $cache;

return $this;
}

public static function initialize(
DataConfig $dataConfig
): self {
$cachedConfig = new self();

$cachedConfig->ruleInferrers = $dataConfig->ruleInferrers;
$cachedConfig->transformers = $dataConfig->transformers;
$cachedConfig->casts = $dataConfig->casts;

$cachedConfig->dataClasses = [];
$cachedConfig->resolvedDataPipelines = [];

$dataConfig->morphMap->merge($cachedConfig->morphMap);

return $cachedConfig;
}
}
Loading

0 comments on commit e9cb661

Please sign in to comment.