lupennat/nova-nested-many

Laravel Nova - Nested Many

v2.2.4 2024-08-27 07:48 UTC

README

  1. 需求
  2. 安装
  3. 使用
  4. 具有NestedMany字段
    1. 依赖项
    2. 传播
    3. 默认子项
    4. 附加选项
    5. 钩子
  5. 可嵌套资源
    1. 可嵌套标题
    2. 可嵌套自定义验证
    3. 可嵌套授权
    4. 可嵌套操作
      1. 可嵌套基本操作
      2. 可嵌套软删除操作
      3. 可嵌套自定义操作
      4. 与Nova操作的差异
      5. 嵌套对象
  6. 递归性
  7. 变更日志
  8. 致谢

需求

  • php: ^7.4 | ^8
  • laravel/nova: ^4

安装

composer require lupennat/nova-nested-many:^2.0

使用

在资源上全局注册特质 HasNestedResource

namespace App\Nova;

use Laravel\Nova\Resource as NovaResource;
use Lupennat\NestedMany\HasNestedResource;

abstract class Resource extends NovaResource
{
    use HasNestedResource;
}

HasMany 一样使用 HasManyNested 字段。

namespace App\Nova;

use Laravel\Nova\Fields\ID;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Gravatar;
use Laravel\Nova\Fields\Password;
// Add use statement here.
use Lupennat\NestedMany\Fields\HasManyNested;

class User extends Resource
{

    public function fields(Request $request)
    {
        return [
            ID::make()->sortable(),

            Gravatar::make(),

            Text::make('Name')
                ->sortable()
                ->rules('required', 'max:255'),

            Text::make('Email')
                ->sortable()
                ->rules('required', 'email', 'max:254')
                ->creationRules('unique:users,email')
                ->updateRules('unique:users,email,{{resourceId}}'),

            Password::make('Password')
                ->onlyOnForms()
                ->creationRules('required', 'string', 'min:6')
                ->updateRules('nullable', 'string', 'min:6'),

            // Add HasManyNested here.
            HasManyNested::make('Posts'),
        ];
    }

}

HasManyNested 默认在 DetailPage、UpdatePage 和 CreatePage 上可见。在 IndexPage 上不可用。

实现合约 Nestable 并使用特质 HasNested 为每个将用于 HasNestedMany 的相关模型。

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Lupennat\NestedMany\Models\Contracts\Nestable;
use Lupennat\NestedMany\Models\HasNested;

class Post extends Model implements Nestable
{
    use HasNested;

}

具有NestedMany字段

依赖项

HasNestedMany 字段支持 Nova dependsOn

HasNestedMany::make('Posts', Post::class)
    ->dependsOn('name', function(HasNestedMany $field, NovaRequest $novaRequest, FormData $formData) {
        if ($formData->name === 'xxx') {
            $field->show();
        } else {
            $field->hide();
        }
    })

传播

HasNestedMany 字段可以将父字段值传播到相关资源。

HasNestedMany::make('Posts', Post::class)->propagate(['name'])

// you can also propagate custom key/value to related resource.
HasNestedMany::make('Posts', Post::class)->propagate(['not_a_field' => 'test'])

在相关资源中,可以通过在请求上调用 getNestedPropagated 方法检索传播的字段。

namespace App\Nova;

use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;

class Post extends Resource
{

    public function fields(NovaRequest $request)
    {
        return array_filter([
            ID::make(),
            BelongsTo::make(__('User'), 'user', User::class),
            Select::section(__('Section'), 'section')
                ->options(['sport' => 'Sport', 'news' => 'News'])
                ->rules('required'),
            Text::title(__('Title'), 'title')
                ->rules('required'),
            $request->getNestedPropagated('name') === 'xxx' ?
                Text::make(__('Extra Field'), 'extra')->hide() : null
        ]);
    }

}

默认子项

您可以使用 defaultChildren 方法在子项为空时生成一组默认的相关资源。

defaultChildren 仅在创建页面上工作。

HasNestedMany::make('Posts', Post::class)
    ->defaultChildren([
        ['title' => 'first post', 'section' => 'sport'],
        ['title' => 'second post', 'section' => 'news'],
    ])

如果您想覆盖现有的子项,可以使用

HasNestedMany::make('Posts', Post::class)
    ->defaultChildren([
        ['title' => 'first post', 'section' => 'sport'],
        ['title' => 'second post', 'section' => 'news'],
    ], true)

附加选项

钩子

您可以在 HasNestedMany 填充数据库之前和之后指定回调。

namespace App\Nova;

use Illuminate\Http\Request;
use Lupennat\NestedMany\Fields\HasManyNested;
use Laravel\Nova\Http\Requests\NovaRequest;

class User extends Resource
{

    public function fields(Request $request)
    {
        return [
            HasNestedMany::make('Posts', Post::class)
                ->beforeFill(function(\App\Models\User $user, NovaRequest $request) {
                    // do stuff
                })
                ->afterFill(function(\App\Models\User $user, NovaRequest $request) {
                    // do stuff
                })
        ];
    }

}

可嵌套资源

资源特质 HasNestedResource 提供与嵌套相关的新的功能。

可嵌套标题

面板和标签视图都使用 nestedTitle 方法检索资源的标题。

如果找不到方法,它将回退到原始的 nova 资源标题

namespace App\Nova;

class User extends Resource
{

    public function nestedTitle()
    {
        return $this->resource->name;
    }

}

可嵌套自定义验证

嵌套资源上的字段验证由 NestedMany 自动管理,对于每个验证错误,属性键都会重新处理并映射到在生成错误的资源的具体字段中向用户显示反馈。

当通过在验证器中添加错误(例如,使用 afterValidation 方法)进行验证时,HasManyNested 无法拦截并重新映射这些错误。可以在请求上通过 getNestedValidationKeyPrefix 方法检索相关资源中错误属性的正确前缀。

namespace App\Nova;

class User extends Resource
{
    /**
     * Handle any post-validation processing.
     *
     * @param \Illuminate\Validation\Validator $validator
     *
     * @return void
     */
    protected static function afterValidation(NovaRequest $request, $validator)
    {
        // do logic to detect error
        $isDisposableEmail = true;
        if($isDisposableEmail) {
            $validator
                ->errors()
                ->add(
                    $request->getNestedValidationKeyPrefix() . 'email',
                    'Temporary emails are forbidden.'
                );
        }
    }

可嵌套授权

Nested Many 将使用 Laravel Nova 资源授权进行创建/更新/删除。您可以在 3 个新方法内定义针对 Nested Many 的不同策略。

namespace App\Nova;

class User extends Resource
{

    public static function authorizedToCreateNested(Request $request)
    {
        return true;
    }

    public function authorizedToUpdateNested(Request $request)
    {
        return false;
    }

    public function authorizedToDeleteNested(Request $request)
    {
        return $this->authorizedToDelete($request);
    }

}

可嵌套操作

您可以通过服务器操作相关内容,并定义 Nested Actions。

Nested 操作可以通过属性 $keepOpened 或方法 keepOpened() 在操作运行后保持模态打开。

可嵌套基本操作

默认情况下,NestedMany 有 3 个基本操作 NestedBasicAddActionNestedBasicDeleteActionNestedBasicRestoreAction,您可以通过资源上的 3 个方法来定制它们。

namespace App\Nova;

use Lupennat\NestedMany\Actions\Basics\NestedBasicAddAction;
use Lupennat\NestedMany\Actions\Basics\NestedBasicDeleteAction;
use Lupennat\NestedMany\Actions\Basics\NestedBasicRestoreAction;
use Laravel\Nova\Http\Requests\NovaRequest;

class Post extends Resource
{

    /**
     * Get the nested create action on the entity.
     */
    public function nestedAddAction(NovaRequest $request): NestedBasicAddAction
    {
        return new \App\Nova\NestedActions\MyCustomAddActionExtendsBasicAddAction();
    }

    /**
     * Get the nested delete action on the entity.
     */
    public function nestedDeleteAction(NovaRequest $request): NestedBasicDeleteAction
    {
        return parent::nestedDeleteAction($request)->withConfirmation();
    }

    /**
     * Get the nested delete action on the entity.
     */
    public function nestedRestoreAction(NovaRequest $request): NestedBasicRestoreAction
    {
        return parent::nestedRestoreAction($request)->withConfirmation();
    }

}

可嵌套软删除操作

NestedDeleteAction 自动支持 softDelete 逻辑(不是真正的 eloquent softDelete),要启用软删除/恢复逻辑,需要在相关模型上设置 protected $nestedHasSoftDelete = true

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Lupennat\NestedMany\Models\Contracts\Nestable;
use Lupennat\NestedMany\Models\HasNested;

class Post extends Model implements Nestable
{
    use HasNested;

    protected $nestedHasSoftDelete = true;

}

可嵌套自定义操作

可以使用 nested-many:action Artisan 命令生成嵌套操作。默认情况下,所有操作都放置在 app/Nova/NestedActions 目录中

php artisan nested-many:action DuplicatePost

您可以通过传递 --destructive 选项生成破坏性操作

php artisan nested-many:action DeleteAllPosts --destructive

嵌套动作与Nova动作有很多共同之处,主要区别在于handle方法应该始终返回一个NestedObject集合。

您可以使用方法$this->getNewNested()生成新的NestedObject

要了解如何定义Nova动作,让我们看一个例子。在这个例子中,我们将定义一个可以复制文章的动作。

<?php

namespace App\Nova\NestedActions;

use Illuminate\Support\Collection;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Http\Requests\NovaRequest;
use Lupennat\NestedMany\Models\Nested;
use Lupennat\NestedMany\Actions\NestedAction;

class DuplicatePost extends NestedAction
{

    /**
     * Handle any post-validation processing.
     *
     * @param \Illuminate\Contracts\Validation\Validator $validator
     * @param \Illuminate\Support\Collection<int,\Lupennat\NestedMany\Models\Nested> $resources
     *
     * @return void
     */
    protected function afterValidation(NovaRequest $request, Collection $resources, $validator)
    {
       // do validation stuff
    }


    /**
     * Perform the action on the given models.
     *
     * @param \Laravel\Nova\Fields\ActionFields $actionFields
     * @param \Illuminate\Support\Collection<int,\Lupennat\NestedMany\Models\Nested> $children
     * @param string|null $selectedUid
     *
     * @return \Illuminate\Support\Collection<int,\Lupennat\NestedMany\Models\Nested>
     */
    public function handle(ActionFields $fields, Collection $children, $selectedUid): Collection
    {
        $selectedNested = $children->where(Nested::UIDFIELD, $selectedUid)->first();

        $children->push(tap($this->getNewNested(), function ($newResource) use ($selectedNested, $fields) {
            foreach ($selectedNested->getAttributes() as $key => $value) {
                if($key !== $selectedNested->getKeyName()) {
                    $newResource->{$key} = $value;
                }
            }
            $newResource->active();
        }));

        return $children;
    }

    /**
     * Get the fields available on the action.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @return array
     */
    public function fields(NovaRequest $request)
    {
        return [];
    }
}

您可以通过方法nestedActions在您的资源中注册自定义的NestedActions

namespace App\Nova;

use Lupennat\NestedMany\Actions\Basics\NestedBasicAddAction;

class Post extends Resource
{
    /**
     * Get the actions available on the entity.
     *
     * @return array<\Lupennat\NestedMany\Actions\NestedAction>
     */
    public function nestedActions(NovaRequest $request): array
    {
        return [
            \App\Nova\NestedActions\DuplicatePost::create()
        ];
    }
}

嵌套对象

当使用NestedActions时,默认情况下,所有Eloquent模型都由扩展Laravel Fluent类的Nested类包装。
所有模型属性都复制到Nested属性中,这样您可以直接修改Nested对象的属性值。

它提供了方便的方法来操作对象,而无需对数据库执行查询。

当调用toModel时,如果注册了修改器,Nested类将对当前对象应用更改到原始Eloquent模型。

对嵌套对象执行的每个更改都不会在用户在父表单页面单击创建/更新按钮之前存储在数据库中。

递归性

HasManyNested支持递归,但请注意,删除“父”资源不会递归到其“子”资源。

例如:父资源 -> 嵌套资源A -> 嵌套资源B

通过删除嵌套资源A,嵌套资源B不会自动从数据库中删除。

您可以通过直接使用Eloquent模型上的删除事件的观察者来解决此问题。

class Parent extends Model {
    protected static function booted(): void
    {
        static::deleted(function (Parent $model) {
            // to propagate event we need to call ->delete() within the model
            foreach ($model->childrenItemA as $childItemA) {
                $childItemA->delete();
            }
        });
    }

    public childrenItemA() {
        return $this->hasMany(ItemA::class);
    }
}

class ItemA extends Model {

    protected static function booted(): void {
        static::deleted(function (ItemA $model) {
            // we can avoid loop we don't need to propagate event
           $model->childrenItemB()->delete()
        });
    }

    public childrenItemB() {
        return $this->hasMany(ItemB::class);
    }
}

class ItemB extends Model {
}

可嵌套的递归对象

递归的HasManyNested关系在NestedActions的子集合中可用,关系是Nested Object数组。您可以使用方法$this->getNewNested('relationName')生成新的关系NestedObject

致谢

NestedForm字段基于原始的Nova Nested Form