/home/bdqbpbxa/api-uniferx.goodface.com.ua/vendor/whitecube/nova-flexible-content/src/Flexible.php
<?php
namespace Whitecube\NovaFlexibleContent;
use Illuminate\Support\Collection;
use Laravel\Nova\Fields\Field;
use Laravel\Nova\Fields\SupportsDependentFields;
use Laravel\Nova\Http\Requests\NovaRequest;
use Whitecube\NovaFlexibleContent\Http\ScopedRequest;
use Whitecube\NovaFlexibleContent\Layouts\Collection as LayoutsCollection;
use Whitecube\NovaFlexibleContent\Layouts\Layout;
use Whitecube\NovaFlexibleContent\Layouts\LayoutInterface;
use Whitecube\NovaFlexibleContent\Layouts\Preset;
use Whitecube\NovaFlexibleContent\Value\Resolver;
use Whitecube\NovaFlexibleContent\Value\ResolverInterface;
class Flexible extends Field
{
use SupportsDependentFields;
/**
* The field's component.
*
* @var string
*/
public $component = 'nova-flexible-content';
/**
* The available layouts collection
*
* @var \Whitecube\NovaFlexibleContent\Layouts\Collection
*/
protected $layouts;
/**
* The currently defined layout groups
*
* @var \Illuminate\Support\Collection
*/
protected $groups;
/**
* The field's value setter & getter
*
* @var \Whitecube\NovaFlexibleContent\Value\ResolverInterface
*/
protected $resolver;
/**
* All the validated attributes
*
* @var array
*/
protected static $validatedKeys = [];
/**
* All the validated attributes
*
* @var \Illuminate\Database\Eloquent\Model
*/
public static $model;
/**
* Create a fresh flexible field instance
*
* @param string $name
* @param string|null $attribute
* @param mixed|null $resolveCallback
* @return void
*/
public function __construct($name, $attribute = null, $resolveCallback = null)
{
parent::__construct($name, $attribute, $resolveCallback);
$this->button(__('Add layout'));
// The original menu as default
$this->menu('flexible-drop-menu');
$this->hideFromIndex();
}
/**
* @param string $component The name of the component to use for the menu
* @param array $data
* @return $this
*/
public function menu($component, $data = [])
{
return $this->withMeta(['menu' => compact('component', 'data')]);
}
/**
* Set the button's label
*
* @param string $label
* @return $this
*/
public function button($label)
{
return $this->withMeta(['button' => $label]);
}
/**
* Make the flexible content take up the full width
* of the form. Labels will sit above
*
* @return mixed
*/
public function fullWidth()
{
return $this->withMeta(['fullWidth' => true]);
}
/**
* Make the flexible content take up the full width
* of the form. Labels will sit above
*
* @return mixed
*/
public function stacked()
{
return $this->fullWidth();
}
/**
* Prevent the 'Add Layout' button from appearing more than once
*
* @return $this
*/
public function limit($limit = 1)
{
return $this->withMeta(['limit' => $limit]);
}
/**
* Confirm remove
*
* @return $this
*/
public function confirmRemove($label = '', $yes = 'Delete', $no = 'Cancel')
{
return $this->withMeta([
'confirmRemove' => true,
'confirmRemoveMessage' => $label,
'confirmRemoveYes' => $yes,
'confirmRemoveNo' => $no,
]);
}
/**
* Set the field's resolver
*
* @param mixed $resolver
* @return $this
*/
public function resolver($resolver)
{
if (is_string($resolver) && is_a($resolver, ResolverInterface::class, true)) {
$resolver = new $resolver();
}
if (! ($resolver instanceof ResolverInterface)) {
throw new \Exception('Resolver Class "'.get_class($resolver).'" does not implement ResolverInterface.');
}
$this->resolver = $resolver;
return $this;
}
/**
* Register a new layout
*
* @param array $arguments
* @return $this
*/
public function addLayout(...$arguments)
{
$count = count($arguments);
if ($count > 1) {
$this->registerLayout(new Layout(...$arguments));
return $this;
}
$layout = $arguments[0];
if (is_string($layout) && is_a($layout, LayoutInterface::class, true)) {
$layout = new $layout();
}
if (! ($layout instanceof LayoutInterface)) {
throw new \Exception('Layout Class "'.get_class($layout).'" does not implement LayoutInterface.');
}
$this->registerLayout($layout);
return $this;
}
/**
* Apply a field configuration preset
*
* @param string|Preset $class
* @param array $params
* @return $this
*/
public function preset($class, $params = [])
{
if (is_string($class)) {
$preset = resolve($class, $params);
} elseif ($class instanceof Preset) {
$preset = $class;
}
$preset->handle($this);
return $this;
}
public function collapsed(bool $value = true)
{
$this->withMeta(['collapsed' => $value]);
return $this;
}
/**
* Push a layout instance into the layouts collection
*
* @param \Whitecube\NovaFlexibleContent\Layouts\LayoutInterface $layout
* @return void
*/
protected function registerLayout(LayoutInterface $layout)
{
if (! $this->layouts) {
$this->layouts = new LayoutsCollection();
$this->withMeta(['layouts' => $this->layouts]);
}
$this->layouts->push($layout);
}
/**
* Resolve the field's value.
*
* @param mixed $resource
* @param string|null $attribute
* @return void
*/
public function resolve($resource, $attribute = null)
{
$attribute = $attribute ?? $this->attribute;
$this->registerOriginModel($resource);
$this->buildGroups($resource, $attribute);
$this->value = $this->resolveGroups($this->groups);
}
/**
* Resolve the field's value for display on index and detail views.
*
* @param mixed $resource
* @param string|null $attribute
* @return void
*/
public function resolveForDisplay($resource, $attribute = null)
{
$attribute = $attribute ?? $this->attribute;
$this->registerOriginModel($resource);
$this->buildGroups($resource, $attribute);
$this->value = $this->resolveGroupsForDisplay($this->groups);
}
/**
* Check showing on detail.
*
* @param NovaRequest $request
* @param $resource
* @return bool
*/
public function isShownOnDetail(NovaRequest $request, $resource): bool
{
$this->layouts = $this->layouts->each(function ($layout) use ($request, $resource) {
$layout->filterForDetail($request, $resource);
});
return parent::isShownOnDetail($request, $resource);
}
/**
* Hydrate the given attribute on the model based on the incoming request.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param string $requestAttribute
* @param object $model
* @param string $attribute
* @return void|\Closure
*/
protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
{
if (! $request->exists($requestAttribute)) {
return;
}
$attribute = $attribute ?? $this->attribute;
$this->registerOriginModel($model);
$this->buildGroups($model, $attribute);
$callbacks = collect($this->syncAndFillGroups($request, $requestAttribute));
$this->value = $this->resolver->set($model, $attribute, $this->groups);
if ($callbacks->isEmpty()) {
return;
}
return function () use ($callbacks) {
$callbacks->each->__invoke();
};
}
/**
* Process an incoming POST Request
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param string $requestAttribute
* @return array
*/
protected function syncAndFillGroups(NovaRequest $request, $requestAttribute): array
{
if (! ($raw = $this->extractValue($request, $requestAttribute))) {
$this->fireRemoveCallbacks(collect());
$this->groups = collect();
return [];
}
$callbacks = [];
$new_groups = collect($raw)->map(function ($item) use ($request, &$callbacks) {
$layout = $item['layout'];
$key = $item['key'];
$attributes = $item['attributes'];
$group = $this->findGroup($key) ?? $this->newGroup($layout, $key);
if (! $group instanceof Layout) {
return [];
}
$scope = ScopedRequest::scopeFrom($request, $attributes, $key);
$callbacks = array_merge($callbacks, $group->fill($scope));
return $group;
})->filter();
$this->fireRemoveCallbacks($new_groups);
$this->groups = $new_groups;
return $callbacks;
}
/**
* Fire's the remove callbacks on the layouts
*
* @param Collection $new_groups This should be (all) the new groups to bne compared against to find the removed groups
*/
protected function fireRemoveCallbacks(Collection $new_groups)
{
$new_group_keys = $new_groups->map(function ($item) {
return $item->inUseKey();
});
$removed_groups = $this->groups->filter(function ($item) use ($new_group_keys) {
return ! $new_group_keys->contains($item->inUseKey());
})->each(function ($group) {
if (method_exists($group, 'fireRemoveCallback')) {
$group->fireRemoveCallback($this);
}
});
}
/**
* Find the flexible's value in given request
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param string $attribute
* @return null|array
*/
protected function extractValue(NovaRequest $request, $attribute)
{
$value = $request[$attribute];
if (! $value) {
return;
}
if (! is_array($value)) {
throw new \Exception('Unable to parse incoming Flexible content, data should be an array.');
}
return $value;
}
/**
* Resolve all contained groups and their fields
*
* @param \Illuminate\Support\Collection $groups
* @return \Illuminate\Support\Collection
*/
protected function resolveGroups($groups)
{
return $groups->map(function ($group) {
return $group->getResolved();
});
}
/**
* Resolve all contained groups and their fields for display on index and
* detail views.
*
* @param \Illuminate\Support\Collection $groups
* @return \Illuminate\Support\Collection
*/
protected function resolveGroupsForDisplay($groups)
{
return $groups->map(function ($group) {
return $group->getResolvedForDisplay();
});
}
/**
* Define the field's actual layout groups (as "base models") based
* on the field's current model & attribute
*
* @param mixed $resource
* @param string $attribute
* @return \Illuminate\Support\Collection
*/
protected function buildGroups($resource, $attribute)
{
if (! $this->resolver) {
$this->resolver(Resolver::class);
}
return $this->groups = $this->resolver->get($resource, $attribute, $this->layouts);
}
/**
* Find an existing group based on its key
*
* @param string $key
* @return \Whitecube\NovaFlexibleContent\Layouts\Layout
*/
protected function findGroup($key)
{
return $this->groups->first(function ($group) use ($key) {
return $group->matches($key);
});
}
/**
* Create a new group based on its key and layout
*
* @param string $layout
* @param string $key
* @return null|\Whitecube\NovaFlexibleContent\Layouts\Layout
*/
protected function newGroup($layout, $key)
{
$layout = $this->layouts->find($layout);
if (! $layout instanceof Layout) {
return null;
}
return $layout->duplicate($key);
}
/**
* Get the validation rules for this field & its contained fields.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @return array
*/
public function getRules(NovaRequest $request)
{
return parent::getRules($request);
}
/**
* Get the creation rules for this field & its contained fields.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @return array|string
*/
public function getCreationRules(NovaRequest $request)
{
return array_merge_recursive(
parent::getCreationRules($request),
$this->getFlexibleRules($request, 'creation')
);
}
/**
* Get the update rules for this field & its contained fields.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @return array
*/
public function getUpdateRules(NovaRequest $request)
{
return array_merge_recursive(
parent::getUpdateRules($request),
$this->getFlexibleRules($request, 'update')
);
}
/**
* Retrieve contained fields rules and assign them to nested array attributes
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param string $specificty
* @return array
*/
protected function getFlexibleRules(NovaRequest $request, $specificty)
{
if (! ($value = $this->extractValue($request, $this->attribute))) {
return [];
}
$rules = $this->generateRules($request, $value, $specificty);
if (! is_a($request, ScopedRequest::class)) {
// We're not in a nested flexible, meaning we're
// assuming the field is located at the root of
// the model's attributes. Therefore, we should now
// register all the collected validation rules for later
// reference (see Http\TransformsFlexibleErrors).
static::registerValidationKeys($rules);
// Then, transform the rules into an array that's actually
// usable by Laravel's Validator.
$rules = $this->getCleanedRules($rules);
}
return $rules;
}
/**
* Format all contained fields rules and return them.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param array $value
* @param string $specificty
* @return array
*/
protected function generateRules(NovaRequest $request, $value, $specificty)
{
return collect($value)->map(function ($item, $key) use ($request, $specificty) {
$group = $this->newGroup($item['layout'], $item['key']);
if (! $group) {
return [];
}
$scope = ScopedRequest::scopeFrom($request, $item['attributes'], $item['key']);
return $group->generateRules($scope, $specificty, $this->attribute.'.'.$key);
})
->collapse()
->all();
}
/**
* Transform Flexible rules array into an actual validator rules array
*
* @param array $rules
* @return array
*/
protected function getCleanedRules(array $rules)
{
return array_map(function ($field) {
return $field['rules'];
}, $rules);
}
/**
* Add validation keys to the valdiatedKeys register, which will be
* used for transforming validation errors later in the request cycle.
*
* @param array $rules
* @return void
*/
protected static function registerValidationKeys(array $rules)
{
$validatedKeys = array_map(function ($field) {
return $field['attribute'];
}, $rules);
static::$validatedKeys = array_merge(
static::$validatedKeys, $validatedKeys
);
}
/**
* Return a previously registered validation key
*
* @param string $key
* @return null|\Whitecube\NovaFlexibleContent\Http\FlexibleAttribute
*/
public static function getValidationKey($key)
{
return static::$validatedKeys[$key] ?? null;
}
/**
* Registers a reference to the origin model for nested & contained fields
*
* @param mixed $model
* @return void
*/
protected function registerOriginModel($model)
{
if (is_a($model, \Laravel\Nova\Resource::class)) {
$model = $model->model();
} elseif (is_a($model, \Whitecube\NovaPage\Pages\Template::class)) {
$model = $model->getOriginal();
}
if (! is_a($model, \Illuminate\Database\Eloquent\Model::class)) {
return;
}
static::$model = $model;
}
/**
* Return the previously registered origin model
*
* @return null|\Illuminate\Database\Eloquent\Model
*/
public static function getOriginModel()
{
return static::$model;
}
}