Skip to content
This repository was archived by the owner on Jul 2, 2025. It is now read-only.
54 changes: 42 additions & 12 deletions packages/Agent/src/Builder/AgentFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ class AgentFactory

public static ?array $fileCacheOptions;

protected ?DatasourceContract $datasource = null;
protected bool $isBuild = false;

protected ?DatasourceContract $computedDatasource = null;

public function __construct(protected array $config)
{
Expand All @@ -46,14 +48,11 @@ public function createAgent(array $config): self
$this->config = array_merge($this->config, $config);
$this->buildLogger();
if ($this->hasEnvSecret) {
$serializableConfig = $this->config;

if (isset($this->config['customizeErrorMessage']) && is_callable($this->config['customizeErrorMessage']) && ! is_string($this->config['customizeErrorMessage'])) {
Cache::put('customizeErrorMessage', new SerializableClosure($this->config['customizeErrorMessage']));
}

unset($serializableConfig['logger'], $serializableConfig['customizeErrorMessage']);
Cache::put('config', $serializableConfig, self::TTL_CONFIG);
Cache::put('config', $this->serializeConfig(), self::TTL_CONFIG);
}

Cache::put('forestAgent', new SerializableClosure(fn () => $this), self::TTL);
Expand Down Expand Up @@ -84,11 +83,11 @@ public function use(string $plugin, array $options = []): self

public function build(): void
{
if ($this->datasource === null) {
$this->datasource = $this->customizer->getDatasource();
if (! $this->isBuild) {
$this->computedDatasource = $this->customizer->getDatasource();
Cache::put('forestAgent', new SerializableClosure(fn () => $this), self::TTL);

self::sendSchema();
$this->isBuild = true;
}
}

Expand Down Expand Up @@ -133,9 +132,8 @@ public static function getDatasource()
{
/** @var self $instance */
$forestAgentClosure = Cache::get('forestAgent');
$instance = $forestAgentClosure();

return $instance->getDatasourceInstance();
return $forestAgentClosure()->getDatasourceInstance();
}

/**
Expand All @@ -162,6 +160,14 @@ public static function sendSchema(bool $force = false): void
}
}

private function serializeConfig(): array
{
$serializableConfig = $this->config;
unset($serializableConfig['logger'], $serializableConfig['customizeErrorMessage']);

return $serializableConfig;
}

private function buildCache(): void
{
if ($this->hasEnvSecret) {
Expand All @@ -172,7 +178,7 @@ private function buildCache(): void
self::$fileCacheOptions = compact('filesystem', 'directory', 'disabledApcuCache');
}

Cache::add('config', $this->config, self::TTL_CONFIG);
Cache::add('config', $this->serializeConfig(), self::TTL_CONFIG);
}
}

Expand All @@ -188,6 +194,30 @@ private function buildLogger(): void

public function getDatasourceInstance(): ?DatasourceContract
{
return $this->datasource;
return $this->computedDatasource;
}

/**
* @codeCoverageIgnore
*/
public function __serialize(): array

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar blocks of code found in 2 locations. Consider refactoring.

{
return [
'hasEnvSecret' => $this->hasEnvSecret,
'isBuild' => $this->isBuild,
'computedDatasource' => $this->computedDatasource,
];
}

/**
* @codeCoverageIgnore
*/
public function __unserialize(array $data): void
{
$this->hasEnvSecret = $data['hasEnvSecret'];
$this->isBuild = $data['isBuild'];
$this->computedDatasource = $data['computedDatasource'];

$this->customizer = new DatasourceCustomizer();
}
}
11 changes: 7 additions & 4 deletions packages/Agent/src/Http/Traits/ErrorHandling.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ForestAdmin\AgentPHP\Agent\Http\Traits;

use ForestAdmin\AgentPHP\Agent\Builder\AgentFactory;
use ForestAdmin\AgentPHP\DatasourceToolkit\Exceptions\ForestException;
use ReflectionClass;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
Expand All @@ -21,17 +22,19 @@ public function getErrorStatus(Throwable $error): int

public function getErrorMessage(Throwable $error): string
{
if ($error instanceof HttpException || is_subclass_of($error, HttpException::class)) {
return $error->getMessage();
}

if ($customizer = AgentFactory::get('customizeErrorMessage')) {
$message = $customizer($error);
if ($message) {
return $message;
}
}

if ($error instanceof HttpException ||
is_subclass_of($error, HttpException::class) ||
$error instanceof ForestException) {
return $error->getMessage();
}

return 'Unexpected error';
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public function processIncludedResources(Scope $scope, $data)
$includedData[$include] = [
'data' => [
'type' => $item->getResourceKey(),
'id' => Id::packId(AgentFactory::get('datasource')->getCollection($this->name), $data),
'id' => Id::packId(AgentFactory::get('datasource')->getCollection($item->getResourceKey()), $item->getData()),
'attributes' => $item->getData(),
],
];
Expand Down
4 changes: 4 additions & 0 deletions packages/Agent/src/Utils/Id.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public static function packId(CollectionContract $collection, array $record): st
throw new ForestException('This collection has no primary key');
}

if (empty($record)) {
throw new ForestException('This record does not exist');
}

return collect($primaryKeyNames)->map(fn ($pk) => $record[$pk])->join('|');
}

Expand Down
42 changes: 19 additions & 23 deletions packages/Agent/src/Utils/QueryStringParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,33 +57,29 @@ public static function parseConditionTree(CollectionContract $collection, Reques
*/
public static function parseProjection(CollectionContract $collection, Request $request): Projection
{
try {
$fields = $request->input('fields.' . $collection->getName());
$fields = $request->input('fields.' . $collection->getName());

if ($fields === null || $fields === '') {
return ProjectionFactory::all($collection);
}
$rootFields = collect(explode(',', $fields));
$explicitRequest = $rootFields->map(
static function ($field) use ($collection, $request) {
$field = trim($field);
$column = $collection->getFields()->get($field);

if (null !== $column && $column->getType() === 'PolymorphicManyToOne') {
return $field . ':*';
}

return null !== $column && $column->getType() === 'Column' ?
$field : $field . ':' . $request->input("fields.$field");
if ($fields === null || $fields === '') {
return ProjectionFactory::all($collection);
}
$rootFields = collect(explode(',', $fields));
$explicitRequest = $rootFields->map(
static function ($field) use ($collection, $request) {
$field = trim($field);
$column = $collection->getFields()->get($field);

if (null !== $column && $column->getType() === 'PolymorphicManyToOne') {
return $field . ':*';
}
);

ProjectionValidator::validate($collection, new Projection($explicitRequest->toArray()));
return null !== $column && $column->getType() === 'Column' ?
$field : $field . ':' . $request->input("fields.$field");
}
);

ProjectionValidator::validate($collection, new Projection($explicitRequest->toArray()));

return new Projection($explicitRequest->all());
} catch (\Exception $e) {
throw new ForestException('Invalid projection');
}
return new Projection($explicitRequest->all());
}

public static function parseProjectionWithPks(CollectionContract $collection, Request $request): Projection
Expand Down
15 changes: 11 additions & 4 deletions packages/BaseDatasource/src/BaseDatasource.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ private function makeOrm(array $databaseConfig): void
{
$this->orm = new Manager();
$this->orm->addConnection($databaseConfig);
$this->orm->bootEloquent();
}

private function makeDoctrineConnection(array $databaseConfig): void
Expand Down Expand Up @@ -84,16 +83,24 @@ private function makeDoctrineConnection(array $databaseConfig): void
/**
* @codeCoverageIgnore
*/
public function __sleep(): array
public function __serialize(): array
{
return ['collections', 'charts', 'databaseConfig'];
return [
'collections' => $this->collections,
'charts' => $this->charts,
'databaseConfig' => $this->databaseConfig,
];
}

/**
* @codeCoverageIgnore
*/
public function __wakeup(): void
public function __unserialize(array $data): void
{
$this->collections = $data['collections'];
$this->charts = $data['charts'];
$this->databaseConfig = $data['databaseConfig'];

$this->makeOrm($this->databaseConfig);
$this->makeDoctrineConnection($this->databaseConfig);
}
Expand Down
29 changes: 29 additions & 0 deletions packages/DatasourceEloquent/src/EloquentCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,33 @@ private function nullablePolymorphicMorphManyRelationFields($record, $relationNa
// Do nothing
}
}

public function __serialize(): array
{
return [
'fields' => $this->fields,
'actions' => $this->actions,
'segments' => $this->segments,
'charts' => $this->charts,
'schema' => $this->schema,
'dataSource' => $this->dataSource,
'name' => $this->name,
'tableName' => $this->tableName,
'model' => $this->model::class,
];
}

public function __unserialize(array $data): void
{
$this->fields = $data['fields'];
$this->actions = $data['actions'];
$this->segments = $data['segments'];
$this->charts = $data['charts'];
$this->schema = $data['schema'];
$this->dataSource = $data['dataSource'];
$this->name = $data['name'];
$this->tableName = $data['tableName'];
$this->model = new $data['model']();
$this->nativeDriver = null;
}
}
22 changes: 22 additions & 0 deletions packages/DatasourceEloquent/src/EloquentDatasource.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,26 @@ public function getModels(): array
{
return $this->models;
}

/**
* @codeCoverageIgnore
*/
public function __serialize(): array
{
return array_merge(
parent::__serialize(),
['supportPolymorphicRelations' => $this->supportPolymorphicRelations]
);
}

/**
* @codeCoverageIgnore
*/
public function __unserialize(array $data): void
{
parent::__unserialize($data);

$finder = new ClassFinder(config('projectDir'));
$this->models = $finder->getModelsInNamespace('App');
}
}
4 changes: 0 additions & 4 deletions packages/DatasourceToolkit/src/CollectionMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ public function setSegments(array $segments): Collection
*/
public function addField(string $name, ColumnSchema|RelationSchema $field): void
{
if ($this->fields->has($name)) {
throw new ForestException('Field ' . $name . ' already defined in collection');
}

$this->fields->put($name, $field);
}

Expand Down
5 changes: 2 additions & 3 deletions tests/Agent/Http/ForestControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use ForestAdmin\AgentPHP\Agent\Http\ForestController;
use ForestAdmin\AgentPHP\Agent\Http\Request;
use ForestAdmin\AgentPHP\DatasourceToolkit\Datasource;
use ForestAdmin\AgentPHP\DatasourceToolkit\Exceptions\ForestException;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
Expand Down Expand Up @@ -59,7 +58,7 @@
->getMock();
$forestControllerMock->expects($this->once())
->method('getClosure')
->willReturn(fn () => throw new ForestException());
->willReturn(fn () => throw new \Exception());

$result = $forestControllerMock->__invoke($request);

Expand All @@ -72,7 +71,7 @@
[
'errors' => [
[
'name' => 'ForestException',
'name' => 'Exception',
'detail' => 'Unexpected error',
'status' => 500,
],
Expand Down
5 changes: 1 addition & 4 deletions tests/Agent/Http/Traits/ErrorHandlingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@

test('getErrorMessage() should return the original message when error is subclass of HttpException', function () {
$this->buildAgent(new Datasource());
$this->agent->createAgent([
'customizeErrorMessage' => fn ($error) => 'my custom message error',
]);
$trait = $this->getObjectForTrait(ErrorHandling::class);

expect($trait->getErrorMessage(new ForbiddenError('test')))->toEqual('test');
Expand All @@ -67,5 +64,5 @@
$this->buildAgent(new Datasource());
$trait = $this->getObjectForTrait(ErrorHandling::class);

expect($trait->getErrorMessage(new ForestException('test')))->toEqual('Unexpected error');
expect($trait->getErrorMessage(new \Exception('test')))->toEqual('Unexpected error');
});
3 changes: 3 additions & 0 deletions tests/Agent/Routes/Capabilities/CollectionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
'operators' => [
'In',
'Equal',
'Blank',
],
],
[
Expand All @@ -125,6 +126,7 @@
'operators' => [
'In',
'Equal',
'Blank',
],
],
[
Expand All @@ -133,6 +135,7 @@
'operators' => [
'In',
'Equal',
'Blank',
],
],
],
Expand Down
1 change: 0 additions & 1 deletion tests/Agent/Routes/Charts/ApiChartCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
->addChart('myChart', fn ($context, ResultBuilder $resultBuilder) => $resultBuilder->value(34))
->addChart('mySmartChart', fn ($context) => [])
);
$this->invokeProperty($this->agent, 'datasource', 'null');
$this->agent->build();
$_GET['record_id'] = 1;
$request = Request::createFromGlobals();
Expand Down
Loading
Loading