Skip to content

Instantly share code, notes, and snippets.

@ManukMinasyan
Last active March 4, 2025 13:46
Show Gist options
  • Select an option

  • Save ManukMinasyan/d2b367028baab8f5d2c5c6ea80f4c8e8 to your computer and use it in GitHub Desktop.

Select an option

Save ManukMinasyan/d2b367028baab8f5d2c5c6ea80f4c8e8 to your computer and use it in GitHub Desktop.
Spatie Laravel Data - AbstractDataCollectionCast
<?php
namespace DataLens\Casts;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Facades\Crypt;
use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Casts\Uncastable;
use Spatie\LaravelData\Contracts\BaseData;
use Spatie\LaravelData\Contracts\BaseDataCollectable;
use Spatie\LaravelData\Contracts\TransformableData;
use Spatie\LaravelData\DataCollection;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Support\DataConfig;
use Spatie\LaravelData\Transformers\Transformer;
use Spatie\LaravelData\Support\Transformation\TransformationContext;
class AbstractDataCollectionCast implements Cast, Transformer
{
protected DataConfig $dataConfig;
public function __construct(
protected string $dataClass,
protected string $dataCollectionClass = DataCollection::class,
protected array $arguments = []
) {
$this->dataConfig = app(DataConfig::class);
}
public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed
{
if ($value === null) {
return null;
}
// Handle encrypted values
if (is_string($value) && in_array('encrypted', $this->arguments)) {
try {
$value = Crypt::decryptString($value);
} catch (\Exception $e) {
return Uncastable::create();
}
}
// Default empty collection for null values
if ($value === null && in_array('default', $this->arguments)) {
$value = '[]';
}
// Already a properly typed DataCollection
if ($value instanceof $this->dataCollectionClass &&
$value->getDataClass() === $this->dataClass) {
return $value;
}
try {
// Parse string value
if (is_string($value)) {
$data = json_decode($value, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($data)) {
return Uncastable::create();
}
} elseif (is_array($value)) {
$data = $value;
} elseif ($value instanceof Arrayable) {
$data = $value->toArray();
} else {
return Uncastable::create();
}
$dataClass = $this->dataConfig->getDataClass($this->dataClass);
$items = [];
// Process each item in the collection
foreach ($data as $item) {
$processed = $this->castCollectionItem($dataClass, $item, $context);
if ($processed instanceof Uncastable) {
return $processed;
}
$items[] = $processed;
}
return new ($this->dataCollectionClass)($this->dataClass, $items);
} catch (\Exception $e) {
return Uncastable::create();
}
}
/**
* Cast an individual item within the collection
* @throws \Exception
*/
protected function castCollectionItem($dataClass, $item, CreationContext $context): mixed
{
$actualDataClass = $this->dataClass; // Default class
// Handle polymorphic abstract data classes
if ($dataClass->isAbstract) {
if (is_array($item) && isset($item['type'])) {
// Polymorphic serialized format with type/data
$morphedClass = $this->dataConfig->morphMap->getMorphedDataClass($item['type']) ?? $item['type'];
if (!class_exists($morphedClass)) {
return Uncastable::create();
}
$actualDataClass = $morphedClass;
} elseif (is_array($item) && isset($item['class'])) {
// Alternative format with class/attributes
$class = $item['class'];
if (!class_exists($class)) {
return Uncastable::create();
}
$actualDataClass = $class;
$item = $item['attributes'] ?? [];
} elseif ($item instanceof BaseData) {
// Already a data object, just return it
return $item;
}
}
// Use the from method on the data class with the current context
if (is_array($item)) {
return $actualDataClass::from($item);
}
return Uncastable::create();
}
/**
* Transform method for serializing the DataCollection back
*/
public function transform(DataProperty $property, mixed $value, TransformationContext $context): mixed
{
if ($value === null) {
return null;
}
// Handle different types of collections
if ($value instanceof BaseDataCollectable && $value instanceof TransformableData) {
$items = $value->all();
} elseif ($value instanceof Arrayable) {
$items = $value->toArray();
} elseif (is_array($value)) {
$items = $value;
} else {
return $value; // Can't transform
}
$dataClass = $this->dataConfig->getDataClass($this->dataClass);
$result = [];
// Transform each item in the collection
foreach ($items as $item) {
if ($dataClass->isAbstract && $item instanceof BaseData) {
$class = get_class($item);
$alias = $this->dataConfig->morphMap->getDataClassAlias($class) ?? $class;
// Store with type information for abstract classes
$result[] = [
'type' => $alias,
'data' => $item->toArray(),
];
} elseif ($item instanceof BaseData) {
$result[] = $item->toArray();
} elseif (is_array($item)) {
$result[] = $item;
} else {
$result[] = (string) $item;
}
}
// Handle encryption if needed
if (in_array('encrypted', $this->arguments)) {
return Crypt::encryptString(json_encode($result));
}
return $result;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment