Skip to content

Commit

Permalink
Implemented generated fields using callables
Browse files Browse the repository at this point in the history
  • Loading branch information
drtheuns committed Feb 17, 2020
1 parent b718d63 commit b511655
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 135 deletions.
6 changes: 6 additions & 0 deletions src/Apitizer/Concerns/HasFields.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Apitizer\Types\EnumField;
use Apitizer\Types\DateTimeField;
use Apitizer\Transformers\CastValue;
use Apitizer\Types\GeneratedField;

trait HasFields
{
Expand Down Expand Up @@ -61,4 +62,9 @@ protected function enum(string $key, array $enum, string $type = 'string'): Enum
return (new EnumField($this, $key, $enum, $type))
->transform(new CastValue);
}

protected function generatedField(string $type, callable $generator): GeneratedField
{
return new GeneratedField($this, $type, $generator);
}
}
4 changes: 2 additions & 2 deletions src/Apitizer/Interpreter/QueryInterpreter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Apitizer\Types\FetchSpec;
use Apitizer\QueryBuilder;
use Apitizer\Types\Field;
use Apitizer\Types\AbstractField;
use Apitizer\Types\Association;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Expand Down Expand Up @@ -42,7 +42,7 @@ private function applySelect(Builder $query, array $fields, array $additionalSel
$selectKeys = array_merge([$query->getModel()->getKeyName()], $additionalSelects);

foreach ($fields as $fieldOrAssoc) {
if ($fieldOrAssoc instanceof Field) {
if ($fieldOrAssoc instanceof AbstractField) {
// Also load any of the selected keys.
$selectKeys[] = $fieldOrAssoc->getKey();
} else if ($fieldOrAssoc instanceof Association) {
Expand Down
8 changes: 4 additions & 4 deletions src/Apitizer/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
use Apitizer\Types\Apidoc;
use Apitizer\Types\Association;
use Apitizer\Types\FetchSpec;
use Apitizer\Types\Field;
use Apitizer\Types\AbstractField;
use Apitizer\Types\Filter;
use Apitizer\Types\Sort;
use Illuminate\Database\Eloquent\Builder;
Expand Down Expand Up @@ -65,7 +65,7 @@ abstract class QueryBuilder
/**
* The result of the fields() callback.
*
* @var Field[]|Association[]
* @var AbstractField[]|Association[]
*/
protected $availableFields;

Expand Down Expand Up @@ -412,7 +412,7 @@ protected function validateRequestInput(ParsedInput $unvalidatedInput): FetchSpe
/**
* Validate the fields that were requested by the client.
*
* @return [string => Field|Association]
* @return [string => AbstractField|Association]
*/
protected function getValidatedFields(array $unvalidatedFields, array $availableFields): array
{
Expand Down Expand Up @@ -528,7 +528,7 @@ public function getFilters(): array
public function getOnlyFields(): array
{
return array_filter($this->getFields(), function ($field) {
return $field instanceof Field;
return $field instanceof AbstractField;
});
}

Expand Down
5 changes: 3 additions & 2 deletions src/Apitizer/Support/DefinitionHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
namespace Apitizer\Support;

use Apitizer\QueryBuilder;
use Apitizer\Types\Field;
use Apitizer\Types\AbstractField;
use Apitizer\Types\Association;
use Apitizer\Exceptions\DefinitionException;
use Apitizer\Types\Field;
use Apitizer\Types\Filter;
use Apitizer\Types\Sort;
use Illuminate\Database\Eloquent\Relations\Relation as EloquentRelation;
Expand Down Expand Up @@ -53,7 +54,7 @@ static function validateField(QueryBuilder $queryBuilder, string $name, $field)
$field = new Field($queryBuilder, $field, 'any');
}

if (!$field instanceof Field) {
if (!$field instanceof AbstractField) {
throw DefinitionException::fieldDefinitionExpected($queryBuilder, $name, $field);
}

Expand Down
4 changes: 2 additions & 2 deletions src/Apitizer/Transformers/CastValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Apitizer\Transformers;

use Apitizer\Types\Field;
use Apitizer\Types\AbstractField;
use Apitizer\Support\TypeCaster;

class CastValue
Expand All @@ -14,7 +14,7 @@ public function __construct(string $format = null)
$this->$format = $format;
}

public function __invoke($value, Field $field)
public function __invoke($value, $row, AbstractField $field)
{
return TypeCaster::cast($value, $field->getType(), $this->format);
}
Expand Down
3 changes: 1 addition & 2 deletions src/Apitizer/Transformers/DateTimeFormat.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Apitizer\Transformers;

use DateTimeInterface;
use Apitizer\Types\Field;

class DateTimeFormat
{
Expand All @@ -14,7 +13,7 @@ public function __construct(string $format = 'Y-m-d H:i:s')
$this->format = $format;
}

public function __invoke(DateTimeInterface $value, Field $field)
public function __invoke(DateTimeInterface $value)
{
return $value->format($this->format);
}
Expand Down
178 changes: 178 additions & 0 deletions src/Apitizer/Types/AbstractField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?php

namespace Apitizer\Types;

use Apitizer\Exceptions\CastException;
use Apitizer\Exceptions\InvalidInputException;
use Apitizer\Exceptions\InvalidOutputException;
use Apitizer\Policies\PolicyFailed;
use Apitizer\QueryBuilder;
use ArrayAccess;

abstract class AbstractField extends Factory
{
use Concerns\FetchesValueFromRow,
Concerns\HasPolicy;

/**
* The internal type that is used for this field.
*
* @var string
*/
protected $type;

/**
* Whether or not this field can be null.
*
* @var bool
*/
protected $nullable = false;

/**
* The transformation callables that are called when the field is rendered.
*
* @var callable[]
*/
protected $transformers = [];

/**
* A callback that fetches the value for the current field during the
* rendering process. For fields that use the Eloquent model, this function
* would get the value at some key from the model.
*
* @param ArrayAccess|array|object $row
*
* @return mixed the value that should be rendered.
*/
abstract protected function getValue($row);

/**
* Add a transformation function that will be applied when rendering the
* value. Transformers are applied in insertion order.
*
* @param callable $callable The transformation function. This will receive
* three parameters:
* 1. The value that should be transformed.
* 2. The entire row that is currently being transformed.
* 3. The Field instance (this object).
*
* @return $this
*/
public function transform(callable $callable): self
{
$this->transformers[] = $callable;

return $this;
}

/**
* Set the field to nullable.
*
* @param bool $isNullable
*
* @return $this
*/
public function nullable(bool $isNullable = true): self
{
$this->nullable = $isNullable;

return $this;
}

/**
* Render a row of data.
*
* @param ArrayAccess|array|object $row
*
* @throws InvalidOutputException if the value does not adhere to the
* requirements set by the field. For example, if the field is not
* nullable but the value is null, this will throw an error. Enum
* field may also throw an error if the value is not in the enum.
*
* @return mixed the transformed value.
*/
public function render($row)
{
$value = $this->validateValue($this->getValue($row), $row);

if (! $this->passesPolicy($value, $row, $this)) {
return new PolicyFailed;
}

return $this->applyTransformers($value, $row);
}

/**
* Apply all the transformers in insertion order.
*
* @param mixed $value the value to transform.
*
* @return mixed the transformed value
*/
protected function applyTransformers($value, $row)
{
foreach ($this->transformers as $transformer) {
try {
$value = call_user_func($transformer, $value, $row, $this);
} catch (CastException $e) {
$e = InvalidOutputException::castError($this, $e, $row);
$this->getQueryBuilder()->handleException($e);

// If the error is ignored, continuing transformations will likely
// generate more unexpected errors, so we'll stop here.
$value = null;
break;
}
}

return $value;
}

/**
* Validate that the value from the row is valid according to the current
* field type.
*
* @param mixed $value
* @param mixed $row
*
* @throws InvalidOutputException
*
* @return mixed the value if it was valid.
*/
protected function validateValue($value, $row)
{
if (is_null($value) && !$this->isNullable()) {
throw InvalidOutputException::fieldIsNull($this, $row);
}

return $value;
}

public function getType(): string
{
return $this->type;
}

public function isNullable(): bool
{
return $this->nullable;
}

/**
* Used when printing the api documentation.
*
* This is a separate function to allow specialized field types from having
* deviating types vs how they are displayed, such as enums.
*/
public function printType(): string
{
return $this->typeOrNull($this->getType());
}

protected function typeOrNull(string $type): string
{
return $this->isNullable()
? "$type or null"
: $type;
}
}
2 changes: 1 addition & 1 deletion src/Apitizer/Types/Apidoc.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public function __construct(QueryBuilder $queryBuilder)
$this->setName($this->guessQueryBuilderResourceName());

foreach ($queryBuilder->getFields() as $field) {
if ($field instanceof Field) {
if ($field instanceof AbstractField) {
$this->fields[] = $field;
}

Expand Down
Loading

0 comments on commit b511655

Please sign in to comment.