570 lines
16 KiB
PHP
570 lines
16 KiB
PHP
<?php
|
|
|
|
namespace Laravel\Nova\Fields;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Str;
|
|
use Laravel\Nova\Contracts\FilterableField;
|
|
use Laravel\Nova\Contracts\QueryBuilder;
|
|
use Laravel\Nova\Contracts\RelatableField;
|
|
use Laravel\Nova\Fields\Filters\EloquentFilter;
|
|
use Laravel\Nova\Http\Requests\NovaRequest;
|
|
use Laravel\Nova\Http\Requests\ResourceIndexRequest;
|
|
use Laravel\Nova\Resource;
|
|
use Laravel\Nova\Rules\Relatable;
|
|
use Laravel\Nova\TrashedStatus;
|
|
use Laravel\Nova\Util;
|
|
|
|
/**
|
|
* @method static static make(mixed $name, string|null $attribute = null, string|null $resource = null)
|
|
*/
|
|
class BelongsTo extends Field implements FilterableField, RelatableField
|
|
{
|
|
use AssociatableRelation;
|
|
use DeterminesIfCreateRelationCanBeShown;
|
|
use EloquentFilterable;
|
|
use FormatsRelatableDisplayValues;
|
|
use Peekable;
|
|
use ResolvesReverseRelation;
|
|
use Searchable;
|
|
use SupportsDependentFields;
|
|
|
|
/**
|
|
* The field's component.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $component = 'belongs-to-field';
|
|
|
|
/**
|
|
* The class name of the related resource.
|
|
*
|
|
* @var class-string<\Laravel\Nova\Resource>
|
|
*/
|
|
public $resourceClass;
|
|
|
|
/**
|
|
* The URI key of the related resource.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $resourceName;
|
|
|
|
/**
|
|
* The resolved BelongsTo Resource.
|
|
*
|
|
* @var \Laravel\Nova\Resource|null
|
|
*/
|
|
public $belongsToResource;
|
|
|
|
/**
|
|
* The name of the Eloquent "belongs to" relationship.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $belongsToRelationship;
|
|
|
|
/**
|
|
* The key of the related Eloquent model.
|
|
*
|
|
* @var string|int|null
|
|
*/
|
|
public $belongsToId;
|
|
|
|
/**
|
|
* Indicates if the related resource can be viewed.
|
|
*
|
|
* @var bool|null
|
|
*/
|
|
public $viewable;
|
|
|
|
/**
|
|
* The callback that should be run when the field is filled.
|
|
*
|
|
* @var \Closure(\Laravel\Nova\Http\Requests\NovaRequest, mixed):void
|
|
*/
|
|
public $filledCallback;
|
|
|
|
/**
|
|
* The attribute that is the inverse of this relationship.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $inverse;
|
|
|
|
/**
|
|
* The displayable singular label of the relation.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $singularLabel;
|
|
|
|
/**
|
|
* Indicates whether the field should display the "With Trashed" option.
|
|
*
|
|
* @var bool
|
|
*/
|
|
public $displaysWithTrashed = true;
|
|
|
|
/**
|
|
* Create a new field.
|
|
*
|
|
* @param string $name
|
|
* @param string|null $attribute
|
|
* @param class-string<\Laravel\Nova\Resource>|null $resource
|
|
* @return void
|
|
*/
|
|
public function __construct($name, $attribute = null, $resource = null)
|
|
{
|
|
parent::__construct($name, $attribute);
|
|
|
|
$resource = $resource ?? ResourceRelationshipGuesser::guessResource($name);
|
|
|
|
$this->resourceClass = $resource;
|
|
$this->resourceName = $resource::uriKey();
|
|
$this->belongsToRelationship = $this->attribute = $attribute ?? ResourceRelationshipGuesser::guessRelation($name);
|
|
$this->singularLabel = $name;
|
|
}
|
|
|
|
/**
|
|
* Get the relationship name.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function relationshipName()
|
|
{
|
|
return $this->belongsToRelationship;
|
|
}
|
|
|
|
/**
|
|
* Get the relationship type.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function relationshipType()
|
|
{
|
|
return 'belongsTo';
|
|
}
|
|
|
|
/**
|
|
* Determine if the field should be displayed for the given request.
|
|
*
|
|
* @param \Illuminate\Http\Request&\Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @return bool
|
|
*/
|
|
public function authorize(Request $request)
|
|
{
|
|
return $this->isNotRedundant($request) && parent::authorize($request);
|
|
}
|
|
|
|
/**
|
|
* Determine if the field is not redundant.
|
|
*
|
|
* Ex: Is this a "user" belongs to field in a blog post list being shown on the "user" detail page.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @return bool
|
|
*/
|
|
public function isNotRedundant(NovaRequest $request)
|
|
{
|
|
return ! $request instanceof ResourceIndexRequest || ! $this->isReverseRelation($request);
|
|
}
|
|
|
|
/**
|
|
* Resolve the field's value.
|
|
*
|
|
* @param mixed $resource
|
|
* @param string|null $attribute
|
|
* @return void
|
|
*/
|
|
public function resolve($resource, $attribute = null)
|
|
{
|
|
$value = null;
|
|
|
|
if ($resource instanceof Resource || $resource instanceof Model) {
|
|
if ($resource->relationLoaded($this->attribute)) {
|
|
$value = $resource->getRelation($this->attribute);
|
|
} else {
|
|
$value = $resource->{$this->attribute}()->withoutGlobalScopes()->getResults();
|
|
}
|
|
}
|
|
|
|
if ($value) {
|
|
$this->belongsToResource = new $this->resourceClass($value);
|
|
|
|
$this->belongsToId = Util::safeInt($value->getKey());
|
|
|
|
$this->value = $this->formatDisplayValue($this->belongsToResource);
|
|
|
|
$this->viewable = ($this->viewable ?? true) && $this->belongsToResource->authorizedToView(app(NovaRequest::class));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve dependent field value.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @return mixed
|
|
*/
|
|
public function resolveDependentValue(NovaRequest $request)
|
|
{
|
|
return $this->belongsToId ?? $this->resolveDefaultValue($request);
|
|
}
|
|
|
|
/**
|
|
* Define the callback that should be used to resolve the field's value.
|
|
*
|
|
* @param callable $displayCallback
|
|
* @return $this
|
|
*/
|
|
public function displayUsing(callable $displayCallback)
|
|
{
|
|
return $this->display($displayCallback);
|
|
}
|
|
|
|
/**
|
|
* Get the validation rules for this field.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @return array
|
|
*/
|
|
public function getRules(NovaRequest $request)
|
|
{
|
|
$query = $this->buildAssociatableQuery(
|
|
$request, $request->{$this->attribute.'_trashed'} === 'true'
|
|
)->toBase();
|
|
|
|
return array_merge_recursive(parent::getRules($request), [
|
|
$this->attribute => array_filter([
|
|
$this->nullable ? 'nullable' : 'required',
|
|
new Relatable($request, $query),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Hydrate the given attribute on the model based on the incoming request.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
|
* @return void
|
|
*/
|
|
public function fill(NovaRequest $request, $model)
|
|
{
|
|
$foreignKey = $this->getRelationForeignKeyName($model->{$this->attribute}());
|
|
|
|
parent::fillInto($request, $model, $foreignKey);
|
|
|
|
if ($model->isDirty($foreignKey)) {
|
|
$model->unsetRelation($this->attribute);
|
|
}
|
|
|
|
if (is_callable($this->filledCallback)) {
|
|
call_user_func($this->filledCallback, $request, $model);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hydrate the given attribute on the model based on the incoming request.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
|
* @return mixed
|
|
*/
|
|
public function fillForAction(NovaRequest $request, $model)
|
|
{
|
|
if ($request->exists($this->attribute)) {
|
|
$value = $request[$this->attribute];
|
|
|
|
$model->{$this->attribute} = $this->resourceClass::newModel()->query()->find($value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hydrate the given attribute on the model based on the incoming request.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @param string $requestAttribute
|
|
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
|
* @param string $attribute
|
|
* @return mixed
|
|
*/
|
|
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
|
|
{
|
|
if ($request->exists($requestAttribute)) {
|
|
$value = $request[$requestAttribute];
|
|
|
|
$relation = Relation::noConstraints(function () use ($model) {
|
|
return $model->{$this->attribute}();
|
|
});
|
|
|
|
if ($this->isValidNullValue($value)) {
|
|
$relation->dissociate();
|
|
} else {
|
|
$relation->associate($relation->getQuery()->withoutGlobalScopes()->find($value));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build an associatable query for the field.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @param bool $withTrashed
|
|
* @return \Laravel\Nova\Contracts\QueryBuilder
|
|
*/
|
|
public function buildAssociatableQuery(NovaRequest $request, $withTrashed = false)
|
|
{
|
|
$model = forward_static_call(
|
|
[$resourceClass = $this->resourceClass, 'newModel']
|
|
);
|
|
|
|
$query = app()->make(QueryBuilder::class, [$resourceClass]);
|
|
|
|
$request->first === 'true'
|
|
? $query->whereKey($model->newQueryWithoutScopes(), $request->current)
|
|
: $query->search(
|
|
$request, $model->newQuery(), $request->search,
|
|
[], [], TrashedStatus::fromBoolean($withTrashed)
|
|
);
|
|
|
|
return $query->tap(function ($query) use ($request, $model) {
|
|
if (is_callable($this->relatableQueryCallback)) {
|
|
call_user_func($this->relatableQueryCallback, $request, $query);
|
|
|
|
return;
|
|
}
|
|
|
|
forward_static_call($this->associatableQueryCallable($request, $model), $request, $query, $this);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the associatable query method name.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @param \Illuminate\Database\Eloquent\Model $model
|
|
* @return array
|
|
*/
|
|
protected function associatableQueryCallable(NovaRequest $request, $model)
|
|
{
|
|
return ($method = $this->associatableQueryMethod($request, $model))
|
|
? [$request->resource(), $method]
|
|
: [$this->resourceClass, 'relatableQuery'];
|
|
}
|
|
|
|
/**
|
|
* Get the associatable query method name.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @param \Illuminate\Database\Eloquent\Model $model
|
|
* @return string|null
|
|
*/
|
|
protected function associatableQueryMethod(NovaRequest $request, $model)
|
|
{
|
|
$method = 'relatable'.Str::plural(class_basename($model));
|
|
|
|
if (method_exists($request->resource(), $method)) {
|
|
return $method;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format the given associatable resource.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @param mixed $resource
|
|
* @return array
|
|
*/
|
|
public function formatAssociatableResource(NovaRequest $request, $resource)
|
|
{
|
|
return array_filter([
|
|
'avatar' => $resource->resolveAvatarUrl($request),
|
|
'display' => $this->formatDisplayValue($resource),
|
|
'subtitle' => $resource->subtitle(),
|
|
'value' => Util::safeInt($resource->getKey()),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Specify if the related resource can be viewed.
|
|
*
|
|
* @param bool $value
|
|
* @return $this
|
|
*/
|
|
public function viewable($value = true)
|
|
{
|
|
$this->viewable = $value;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Specify a callback that should be run when the field is filled.
|
|
*
|
|
* @param \Closure(\Laravel\Nova\Http\Requests\NovaRequest, mixed):void $callback
|
|
* @return $this
|
|
*/
|
|
public function filled($callback)
|
|
{
|
|
$this->filledCallback = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the value for the field.
|
|
*
|
|
* @param mixed $value
|
|
* @return void
|
|
*/
|
|
public function setValue($value)
|
|
{
|
|
$this->belongsToId = Util::safeInt($value);
|
|
$this->value = $value;
|
|
}
|
|
|
|
/**
|
|
* Set the attribute name of the inverse of the relationship.
|
|
*
|
|
* @param string $inverse
|
|
* @return $this
|
|
*/
|
|
public function inverse($inverse)
|
|
{
|
|
$this->inverse = $inverse;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the displayable singular label of the resource.
|
|
*
|
|
* @param string $singularLabel
|
|
* @return $this
|
|
*/
|
|
public function singularLabel($singularLabel)
|
|
{
|
|
$this->singularLabel = $singularLabel;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* hides the "With Trashed" option.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function withoutTrashed()
|
|
{
|
|
$this->displaysWithTrashed = false;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Return the sortable uri key for the field.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function sortableUriKey()
|
|
{
|
|
$request = app(NovaRequest::class);
|
|
|
|
return $this->getRelationForeignKeyName($request->newResource()->resource->{$this->attribute}());
|
|
}
|
|
|
|
/**
|
|
* Make the field filter.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @return \Laravel\Nova\Fields\Filters\Filter|null
|
|
*/
|
|
protected function makeFilter(NovaRequest $request)
|
|
{
|
|
if ($request->viaRelationship()
|
|
&& ($request->relationshipType ?? null) === 'hasMany'
|
|
&& $this->resourceClass::uriKey() === $request->viaResource
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return new EloquentFilter($this);
|
|
}
|
|
|
|
/**
|
|
* Define filterable attribute.
|
|
*
|
|
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
|
* @return string
|
|
*/
|
|
protected function filterableAttribute(NovaRequest $request)
|
|
{
|
|
return $this->getRelationForeignKeyName($request->newResource()->resource->{$this->attribute}());
|
|
}
|
|
|
|
/**
|
|
* Define the default filterable callback.
|
|
*
|
|
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):void
|
|
*/
|
|
protected function defaultFilterableCallback()
|
|
{
|
|
return function (NovaRequest $request, $query, $value, $attribute) {
|
|
$query->where($attribute, '=', $value);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Prepare the field for JSON serialization.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function serializeForFilter()
|
|
{
|
|
return transform($this->jsonSerialize(), function ($field) {
|
|
return [
|
|
'debounce' => $field['debounce'],
|
|
'displaysWithTrashed' => $field['displaysWithTrashed'],
|
|
'label' => $this->resourceClass::label(),
|
|
'resourceName' => $field['resourceName'],
|
|
'searchable' => $field['searchable'],
|
|
'withSubtitles' => $field['withSubtitles'],
|
|
'uniqueKey' => $field['uniqueKey'],
|
|
];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Prepare the field for JSON serialization.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function jsonSerialize(): array
|
|
{
|
|
return with(app(NovaRequest::class), function ($request) {
|
|
$viewable = ! is_null($this->viewable) ? $this->viewable : $this->resourceClass::authorizedToViewAny($request);
|
|
|
|
return array_merge([
|
|
'belongsToId' => $this->belongsToId,
|
|
'relationshipType' => $this->relationshipType(),
|
|
'belongsToRelationship' => $this->belongsToRelationship,
|
|
'debounce' => $this->debounce,
|
|
'displaysWithTrashed' => $this->displaysWithTrashed,
|
|
'label' => $this->resourceClass::label(),
|
|
'peekable' => $this->isPeekable($request),
|
|
'hasFieldsToPeekAt' => $this->hasFieldsToPeekAt($request),
|
|
'resourceName' => $this->resourceName,
|
|
'reverse' => $this->isReverseRelation($request),
|
|
'searchable' => $this->isSearchable($request),
|
|
'withSubtitles' => $this->withSubtitles,
|
|
'showCreateRelationButton' => $this->createRelationShouldBeShown($request),
|
|
'singularLabel' => $this->singularLabel,
|
|
'viewable' => $viewable,
|
|
], parent::jsonSerialize());
|
|
});
|
|
}
|
|
}
|