Skip to content

Commit

Permalink
feat: add enum handler (#43)
Browse files Browse the repository at this point in the history
* ci: load database migrations when running `phpstan`

* feat: add handler for `BackedEnum` type

resolves #33

* Fix styling [skip-ci]

* style: add type for `HasFactory` trait

* Fix styling [skip-ci]

* fix: skip if `ReflectionEnum` does not exist

* test: run enum tests only for php 8.1+

* Fix styling [skip-ci]

---------

Co-authored-by: marijoo <[email protected]>
  • Loading branch information
marijoo and marijoo authored Aug 20, 2024
1 parent 3dbc5ac commit 59cbab9
Show file tree
Hide file tree
Showing 17 changed files with 315 additions and 31 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ And it’s low profile: If you don't like it, just [remove the `HasMeta` Trait](
- [Deleting Metadata](#deleting-metadata)
- [Performance](#performance)
- [Configuration](#configuration)
- [Enum Support](#enum-support)
- [UUID and ULID Support](#uuid-and-ulid-support)

## Installation
Expand Down Expand Up @@ -639,6 +640,23 @@ There is no need to configure anything but if you like, you can publish the conf
php artisan vendor:publish --tag="multiplex-config"
```

## Enum Support

Multiplex supports [backed enumerations](https://www.php.net/manual/en/language.enumerations.backed.php) introduced in PHP 8.1 whereas basic enumerations would not work.

```php
enum SampleEnum: string
{
case Hearts = 'hearts';
case Diamonds = 'diamonds';
}

$model->saveMeta('some_key', SampleEnum::Diamonds);

// true
$model->some_key === SampleEnum::Diamonds;
```

## UUID and ULID Support

If your application uses UUIDs or ULIDs for the model(s) using metadata, you may set the `multiplex.morph_type` setting to `uuid` or `ulid` **before** running the migrations. You might as well set the `MULTIPLEX_MORPH_TYPE` environment variable instead, if you don’t want to publish the configuration file.
Expand Down
1 change: 1 addition & 0 deletions config/multiplex.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
Kolossal\Multiplex\DataType\DateTimeHandler::class,
Kolossal\Multiplex\DataType\DateHandler::class,
Kolossal\Multiplex\DataType\ArrayHandler::class,
Kolossal\Multiplex\DataType\EnumHandler::class,
Kolossal\Multiplex\DataType\ModelHandler::class,
Kolossal\Multiplex\DataType\ModelCollectionHandler::class,
Kolossal\Multiplex\DataType\SerializableHandler::class,
Expand Down
2 changes: 2 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ parameters:
tmpDir: build/phpstan
checkOctaneCompatibility: true
checkModelProperties: true
databaseMigrationsPath:
- database/migrations

78 changes: 78 additions & 0 deletions src/DataType/EnumHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace Kolossal\Multiplex\DataType;

use BackedEnum;
use Exception;
use ReflectionEnum;

/**
* Handle serialization of backed enums.
*/
class EnumHandler implements HandlerInterface
{
/**
* {@inheritdoc}
*/
public function getDataType(): string
{
return 'enum';
}

/**
* {@inheritdoc}
*/
public function canHandleValue($value): bool
{
return $value instanceof BackedEnum && class_exists(ReflectionEnum::class);
}

/**
* Convert the value to a string, so that it can be stored in the database.
*
* @param BackedEnum $enum
*/
public function serializeValue($enum): string
{
if (!$this->canHandleValue($enum)) {
return '';
}

return get_class($enum) . '::' . $enum->value;
}

/**
* {@inheritdoc}
*/
public function unserializeValue(?string $value): mixed
{
// @codeCoverageIgnoreStart
if (!class_exists(ReflectionEnum::class)) {
throw new Exception('Cannot unserialize enum value since \ReflectionEnum is not available. This will only work in PHP >= 8.1.');
}
// @codeCoverageIgnoreEnd

if (is_null($value)) {
return $value;
}

if (strpos($value, '::') === false) {
return null;
}

[$class, $value] = explode('::', $value, 2);

if (!enum_exists($class)) {
return null;
}

if (!(new ReflectionEnum($class))->isBacked()) {
return null;
}

/**
* @var \BackedEnum $class
*/
return $class::tryFrom($value);
}
}
6 changes: 3 additions & 3 deletions src/DataType/ModelCollectionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function unserializeValue(?string $value): mixed
}

/** @var Collection<string, Model> */
$collection = new $data['class']();
$collection = new $data['class'];

/** @var array<string, array<string, string|int|null>> */
$items = $data['items'];
Expand All @@ -83,7 +83,7 @@ public function unserializeValue(?string $value): mixed

// Repopulate collection keys with loaded models.
foreach ($items as $key => $item) {
if (is_null($item['key']) && ($model = new $item['class']()) instanceof Model) {
if (is_null($item['key']) && ($model = new $item['class']) instanceof Model) {
$collection->put($key, $model);
} elseif (isset($models[$item['class']][$item['key']])) {
$collection->put($key, $models[$item['class']][$item['key']]);
Expand Down Expand Up @@ -114,7 +114,7 @@ private function loadModels(array $items): array
// Iterate list of classes and load all records matching a key.
foreach ($classes as $class => $keys) {
/** @var \Illuminate\Database\Eloquent\Model */
$model = new $class();
$model = new $class;

$results[$class] = $model
->whereIn($model->getKeyName(), $keys)
Expand Down
2 changes: 1 addition & 1 deletion src/DataType/ModelHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function unserializeValue(?string $value): mixed

// Return blank instances.
if (strpos($value, '#') === false) {
return new $value();
return new $value;
}

// Fetch specific instances.
Expand Down
2 changes: 1 addition & 1 deletion src/DataType/ScalarHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function getDataType(): string
*/
public function canHandleValue($value): bool
{
return gettype($value) == $this->type;
return gettype($value) === $this->type;
}

/**
Expand Down
7 changes: 0 additions & 7 deletions src/HasMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,6 @@ public function getMetaKeys(): array

/**
* Get the forced typecast for the given meta key if there is any.
*
* @return ?string
*/
public function getCastForMetaKey(string $key): ?string
{
Expand Down Expand Up @@ -470,7 +468,6 @@ public function hasMeta(string $key): bool
* Find current Meta model for the given key.
*
* @param string $key
* @return ?Meta
*/
public function findMeta($key): ?Meta
{
Expand Down Expand Up @@ -538,7 +535,6 @@ public function setMetaAt($key, $value = null, $publishAt = null)
/**
* Set meta values from array of $key => $value pairs.
*
* @param ?Carbon $publishAt
*
* @throws MetaException if invalid keys are used.
*/
Expand All @@ -554,7 +550,6 @@ protected function setMetaFromArray(array $metas, ?Carbon $publishAt = null): Co
*
* @param string $key
* @param mixed $value
* @param ?Carbon $publishAt
*
* @throws MetaException if invalid key is used.
*/
Expand Down Expand Up @@ -637,8 +632,6 @@ protected function setMetaFromString($key, $value, ?Carbon $publishAt = null): M
/**
* Reset the meta changes collection for the given key.
* Resets the entire collection if nothing is passed.
*
* @param ?string $key
*/
public function resetMetaChanges(?string $key = null): Collection
{
Expand Down
9 changes: 3 additions & 6 deletions src/Meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@
class Meta extends Model
{
use HasConfigurableMorphType;

/** @use HasFactory<\Kolossal\Multiplex\Tests\Factories\MetaFactory> */
use HasFactory;

use HasTimestamps;

protected $guarded = [
Expand Down Expand Up @@ -113,8 +116,6 @@ public function metable(): MorphTo

/**
* Set forced type to be used.
*
* @param ?string $value
*/
public function forceType(?string $value): self
{
Expand Down Expand Up @@ -195,8 +196,6 @@ public function getIsPlannedAttribute(): bool

/**
* Retrieve the underlying serialized value.
*
* @return ?string
*/
public function getRawValueAttribute(): ?string
{
Expand Down Expand Up @@ -238,7 +237,6 @@ public function scopeWhereValueNotEmpty(Builder $query): void
* @param Builder<Meta> $query
* @param mixed $value
* @param mixed $operator
* @param ?string $type
*/
public function scopeWhereValue(Builder $query, $value, $operator = '=', ?string $type = null): void
{
Expand All @@ -259,7 +257,6 @@ public function scopeWhereValue(Builder $query, $value, $operator = '=', ?string
*
* @param Builder<Meta> $query
* @param array<mixed> $values
* @param ?string $type
*/
public function scopeWhereValueIn(Builder $query, array $values, ?string $type = null): void
{
Expand Down
4 changes: 2 additions & 2 deletions src/MultiplexServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ public function register(): void
protected function registerDataTypeRegistry(): void
{
$this->app->singleton(Registry::class, function (): Registry {
$registry = new Registry();
$registry = new Registry;
$datatypes = (array) config('multiplex.datatypes', []);

foreach ($datatypes as $handler) {
/** @var \Kolossal\Multiplex\DataType\HandlerInterface */
$handler = new $handler();
$handler = new $handler;
$registry->addHandler($handler);
}

Expand Down
Loading

0 comments on commit 59cbab9

Please sign in to comment.