*/ 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 */ 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()); }); } }