<?php
namespace Laravel\Nova\Fields;
use Laravel\Nova\Exceptions\NovaException;
use Laravel\Nova\Fields\Repeater\Presets\HasMany;
use Laravel\Nova\Fields\Repeater\Presets\JSON;
use Laravel\Nova\Fields\Repeater\Presets\Preset;
use Laravel\Nova\Fields\Repeater\Repeatable;
use Laravel\Nova\Fields\Repeater\RepeatableCollection;
use Laravel\Nova\Http\Requests\NovaRequest;
/**
* @phpstan-import-type TFieldValidationRules from \Laravel\Nova\Fields\Field
*/
class Repeater extends Field
{
/**
* The resource class for the repeater.
*
* @var class-string<\Laravel\Nova\Resource>|null
*/
public $resourceClass;
/**
* The resource name for the repeater.
*
* @var string|null
*/
public $resourceName;
/**
* The field's component.
*
* @var string
*/
public $component = 'repeater-field';
/**
* Indicates if the field label and form element should sit on top of each other.
*
* @var bool
*/
public $stacked = false;
/**
* Indicates whether the field should use all available white-space.
*
* @var bool
*/
public $fullWidth = false;
/**
* The repeatable types used for the Repeater.
*
* @var \Laravel\Nova\Fields\Repeater\RepeatableCollection
*/
public $repeatables;
/**
* @var bool
*/
public $sortable = true;
/**
* @var string|null
*/
public $uniqueField;
/**
* The preset used for the field.
*
* @var \Laravel\Nova\Fields\Repeater\Presets\Preset|null
*/
public $preset;
/**
* Create a new field.
*
* @param string $name
* @param string|null $attribute
* @param (callable(mixed, mixed, ?string):mixed)|null $resolveCallback
*/
public function __construct($name, $attribute = null, callable $resolveCallback = null)
{
parent::__construct($name, $attribute, $resolveCallback);
$this->onlyOnForms();
$this->repeatables = RepeatableCollection::make();
}
/**
* Specify the callback to be executed to retrieve the pivot fields.
*
* @param array<int, \Laravel\Nova\Fields\Repeater\Repeatable> $repeatables
* @return $this
*/
public function repeatables(array $repeatables)
{
foreach ($repeatables as $repeatable) {
$this->repeatables->push($repeatable);
}
return $this;
}
/**
* Set the preset used for the field.
*
* @return $this
*/
public function preset(Preset $preset)
{
$this->preset = $preset;
return $this;
}
/**
* Use the JSON preset for the field.
*
* @return $this
*/
public function asJson()
{
return $this->preset(new JSON);
}
/**
* Use the HasMany preset for the field.
*
* @param class-string<\Laravel\Nova\Resource>|null $resourceClass
* @return $this
*
* @throws \Laravel\Nova\Exceptions\NovaException
*/
public function asHasMany($resourceClass = null)
{
/** @var class-string<\Laravel\Nova\Resource>|null $resource */
$resource = $resourceClass ?? ResourceRelationshipGuesser::guessResource($this->name);
if ($resource) {
$this->resourceClass = $resource;
$this->resourceName = $resource::uriKey();
return $this->preset(new HasMany);
}
throw NovaException::missingResourceForRepeater($this->name);
}
/**
* Return the preset instance for the field.
*
* @return \Laravel\Nova\Fields\Repeater\Presets\Preset
*/
public function getPreset()
{
return $this->preset ?? new JSON;
}
/**
* Resolve the given attribute from the given resource.
*
* @param mixed $resource
* @param string $attribute
* @return mixed
*/
protected function resolveAttribute($resource, $attribute)
{
$request = app(NovaRequest::class);
return $this->getPreset()->get($request, $resource, $attribute, $this->repeatables);
}
/**
* Determine if the field collection contains an ID field.
*/
protected function fieldsContainsIDField(FieldCollection $fields): bool
{
return $fields->contains(function (Field $field) {
return $field instanceof ID && $field->attribute === $this->uniqueField
|| $field instanceof Hidden && $field->attribute === $this->uniqueField;
});
}
/**
* Hydrate the given attribute on the model based on the incoming request.
*
* @param string $requestAttribute
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
* @param string $attribute
* @return \Closure
*/
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
{
return $this->getPreset()->set($request, $requestAttribute, $model, $attribute, $this->repeatables, $this->uniqueField);
}
/**
* Get the creation rules for this field.
*
* @return array<array-key, mixed>
*
* @phpstan-return array<string, TFieldValidationRules>
*/
public function getCreationRules(NovaRequest $request)
{
return array_merge_recursive(parent::getCreationRules($request), $this->formatRules());
}
/**
* Get the update rules for this field.
*
* @return array<array-key, mixed>
*
* @phpstan-return array<string, TFieldValidationRules>
*/
public function getUpdateRules(NovaRequest $request)
{
return array_merge_recursive(parent::getUpdateRules($request), $this->formatRules());
}
/**
* Format available rules.
*
* @return array<array-key, mixed>
*
* @phpstan-return array<string, TFieldValidationRules>
*/
protected function formatRules()
{
$request = app(NovaRequest::class);
if ($request->method() === 'GET') {
return [];
}
return collect($request->{$this->validationKey()})
->map(function ($item) {
return $this->repeatables->findByKey($item['type']);
})
->flatMap(function (Repeatable $repeatable, $index) use ($request) {
return FieldCollection::make($repeatable->fields($request))
->mapWithKeys(function (Field $field) use ($index) {
return ["{$this->validationKey()}.{$index}.fields.{$field->attribute}" => $field->rules];
});
})
->all();
}
/**
* Set the unique database column to use when attempting upserts.
*
* @param string|null $key
* @return $this
*/
public function uniqueField($key)
{
$this->uniqueField = $key;
return $this;
}
/**
* Prepare the field for JSON serialization.
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return array_merge([
'repeatables' => $this->repeatables,
'sortable' => $this->sortable,
], parent::jsonSerialize());
}
}