├── .github
└── FUNDING.yml
├── .gitignore
├── .nvmrc
├── LICENSE.md
├── composer.json
├── dist
├── css
│ └── field.css
├── js
│ ├── field.js
│ └── field.js.LICENSE.txt
└── mix-manifest.json
├── docs
├── demo-2.gif
└── demo.gif
├── nova.mix.js
├── package-lock.json
├── package.json
├── readme.md
├── resources
└── js
│ ├── components
│ ├── DetailField.vue
│ └── FormField.vue
│ ├── field.js
│ └── utils.js
├── src
├── ActionHasDependencies.php
├── DependencyContainer.php
├── FieldServiceProvider.php
├── HasChildFields.php
├── HasDependencies.php
└── Http
│ ├── Controllers
│ └── ActionController.php
│ └── Requests
│ └── ActionRequest.php
└── webpack.mix.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | custom: ['https://www.paypal.com/donate/?hosted_button_id=LHJQRG9FXSYCU']
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /vendor
3 | /node_modules
4 | package-lock.json
5 | composer.phar
6 | composer.lock
7 | phpunit.xml
8 | .phpunit.result.cache
9 | .DS_Store
10 | Thumbs.db
11 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | lts/fermium
2 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Epartment Ecommerce
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alexwenzel/nova-dependency-container",
3 | "description": "A Laravel Nova 4 form container for grouping fields that depend on other field values.",
4 | "keywords": [
5 | "laravel",
6 | "nova",
7 | "nova-4",
8 | "field"
9 | ],
10 | "authors": [
11 | {
12 | "name": "alexwenzel",
13 | "email": "alexander.wenzel.berlin@gmail.com"
14 | },
15 | {
16 | "name": "Epartment E-commerce",
17 | "email": "support@epartment.nl"
18 | }
19 | ],
20 | "license": "MIT",
21 | "require": {
22 | "php": "^8.0",
23 | "laravel/framework": "^8.0|^9.0|^10.0|^11.0|^12.0",
24 | "laravel/nova": "^4.0|^5.0"
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "Alexwenzel\\DependencyContainer\\": "src/"
29 | }
30 | },
31 | "extra": {
32 | "laravel": {
33 | "providers": [
34 | "Alexwenzel\\DependencyContainer\\FieldServiceProvider"
35 | ]
36 | }
37 | },
38 | "config": {
39 | "sort-packages": true
40 | },
41 | "minimum-stability": "dev",
42 | "prefer-stable": true
43 | }
44 |
--------------------------------------------------------------------------------
/dist/css/field.css:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dist/js/field.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * vuex v4.0.2
3 | * (c) 2021 Evan You
4 | * @license MIT
5 | */
6 |
7 | /**
8 | * @license
9 | * Lodash
10 | * Copyright OpenJS Foundation and other contributors
11 | * Released under MIT license
12 | * Based on Underscore.js 1.8.3
13 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
14 | */
15 |
--------------------------------------------------------------------------------
/dist/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/js/field.js": "/js/field.js"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/demo-2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexwenzel/nova-dependency-container/f2ccfc1c14319ba1e6ee1eeff62aed0670706d54/docs/demo-2.gif
--------------------------------------------------------------------------------
/docs/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexwenzel/nova-dependency-container/f2ccfc1c14319ba1e6ee1eeff62aed0670706d54/docs/demo.gif
--------------------------------------------------------------------------------
/nova.mix.js:
--------------------------------------------------------------------------------
1 | const mix = require('laravel-mix')
2 | const webpack = require('webpack')
3 | const path = require('path')
4 |
5 | class NovaExtension {
6 | name() {
7 | return 'nova-extension'
8 | }
9 |
10 | register(name) {
11 | this.name = name
12 | }
13 |
14 | webpackPlugins() {
15 | return new webpack.ProvidePlugin({
16 | _: 'lodash',
17 | Errors: 'form-backend-validation',
18 | })
19 | }
20 |
21 | webpackConfig(webpackConfig) {
22 | webpackConfig.externals = {
23 | vue: 'Vue',
24 | }
25 |
26 | webpackConfig.resolve.alias = {
27 | ...(webpackConfig.resolve.alias || {}),
28 | 'laravel-nova': path.join(__dirname, '../../vendor/laravel/nova/resources/js/mixins/packages.js'),
29 | 'laravel-mixins': path.join(__dirname, '../../vendor/laravel/nova/resources/js/mixins/'),
30 | }
31 |
32 | webpackConfig.output = {
33 | uniqueName: this.name,
34 | }
35 | }
36 | }
37 |
38 | mix.extend('nova', new NovaExtension())
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "npm run development",
5 | "development": "mix",
6 | "watch": "mix watch",
7 | "watch-poll": "mix watch -- --watch-options-poll=1000",
8 | "hot": "mix watch --hot",
9 | "prod": "npm run production",
10 | "production": "mix --production",
11 | "nova:install": "npm --prefix='../../vendor/laravel/nova' ci"
12 | },
13 | "devDependencies": {
14 | "@vue/compiler-sfc": "^3.2.22",
15 | "laravel-mix": "^6.0.41",
16 | "lodash": "^4.17.21",
17 | "postcss": "^8.3.11",
18 | "vue-loader": "^16.8.3"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # nova 4 dependency container
2 |
3 | A Laravel Nova 4 form container for grouping fields that depend on other field values.
4 | Dependencies can be set on any field type or value.
5 |
6 | Features:
7 |
8 | - working form validation inside unlimited nested containers
9 | - support of ebess/advanced-nova-media-library
10 |
11 | This plugin is based on [epartment/nova-dependency-container](https://github.com/epartment/nova-dependency-container)
12 | and only supports **Nova 4.x** and **PHP 8.x**.
13 |
14 | ## Demo
15 |
16 | 
17 |
18 | ## Installation
19 |
20 | The package can be installed through Composer.
21 |
22 | ```bash
23 | composer require alexwenzel/nova-dependency-container
24 | ```
25 |
26 | ## Usage
27 |
28 | 1. Add the `Alexwenzel\DependencyContainer\HasDependencies` trait to your Nova Resource.
29 | 2. Add the `Alexwenzel\DependencyContainer\DependencyContainer` to your Nova Resource `fields()` method.
30 | 3. Add the `Alexwenzel\DependencyContainer\ActionHasDependencies` trait to your Nova Actions that you wish to use
31 | dependencies on.
32 |
33 | ```php
34 | class Page extends Resource
35 | {
36 | use HasDependencies;
37 |
38 | public function fields(Request $request)
39 | {
40 | return [
41 | Select::make('Name format', 'name_format')->options([
42 | 0 => 'First Name',
43 | 1 => 'First Name / Last Name',
44 | 2 => 'Full Name'
45 | ])->displayUsingLabels(),
46 |
47 | DependencyContainer::make([
48 | Text::make('First Name', 'first_name')
49 | ])->dependsOn('name_format', 0),
50 | ];
51 | }
52 | }
53 | ```
54 |
55 | ## Available dependencies
56 |
57 | The package supports these kinds of dependencies:
58 |
59 | 1. `->dependsOn('field', 'value')`
60 | 2. `->dependsOnNot('field', 'value')`
61 | 3. `->dependsOnEmpty('field')`
62 | 4. `->dependsOnNotEmpty('field')`
63 | 5. `->dependsOnNullOrZero('field')`
64 | 6. `->dependsOnIn('field', [array])`
65 | 7. `->dependsOnNotIn('field', [array])`
66 |
67 | These dependencies can be combined by chaining the methods on the `DependencyContainer` field:
68 |
69 | ```php
70 | DependencyContainer::make([
71 | // dependency fields
72 | ])
73 | ->dependsOn('field1', 'value1')
74 | ->dependsOnNotEmpty('field2')
75 | ->dependsOn('field3', 'value3')
76 | ```
77 |
78 | The fields used as dependencies can be of any Laravel Nova field type. Currently only two relation field types are
79 | supported, `BelongsTo` and `MorphTo`.
80 |
81 | Here is an example using a checkbox:
82 |
83 | 
84 |
85 | ## BelongsTo dependency
86 |
87 | If we follow the example of a *Post model belongsTo a User model*, taken from Novas
88 | documentation [BelongsTo](https://nova.laravel.com/docs/2.0/resources/relationships.html#belongsto), the dependency
89 | setup has the following construction.
90 |
91 | We use the singular form of the `belongsTo` resource in lower case, in this example `Post` becomes `post`. Then we
92 | define in dot notation, the property of the resource we want to depend on. In this example we just use the `id`
93 | property, as in `post.id`.
94 |
95 | ```php
96 | BelongsTo::make('Post'),
97 |
98 | DependencyContainer::make([
99 | Boolean::make('Visible')
100 | ])
101 | ->dependsOn('post.id', 2)
102 | ```
103 |
104 | When the `Post` resource with `id` 2 is being selected, a `Boolean` field will appear.
105 |
106 | ## BelongsToMany dependency
107 |
108 | A [BelongsToMany](https://nova.laravel.com/docs/2.0/resources/relationships.html#belongstomany) setup is similar to that
109 | of a [BelongsTo](https://nova.laravel.com/docs/2.0/resources/relationships.html#belongsto).
110 |
111 | The `dependsOn` method should be pointing to the name of the intermediate table. If it is called `role_user`, the setup
112 | should be
113 |
114 | ```php
115 | BelongsToMany::make('Roles')
116 | ->fields(function() {
117 | return [
118 | DependencyContainer::make([
119 | // pivot field rules_all
120 | Boolean::make('Rules All', 'rules_all')
121 | ])
122 | ->dependsOn('role_user', 1)
123 | ]
124 | }),
125 | ```
126 |
127 | If the pivot field name occurs multiple times, consider
128 | using [custom intermediate table models](https://laravel.com/docs/6.x/eloquent-relationships#defining-custom-intermediate-table-models)
129 | and define it in the appropiate model relation methods. The only reliable solution I found was using mutators to get/set
130 | a field which was being used multiple times. Although this may seem ugly, the events which should be fired on the
131 | intermediate model instance, when using an Observer, would work unreliable with every new release of Nova.
132 |
133 | > If Nova becomes reliable firing eloquent events on the intermediate table, I will update this examples with a more
134 | > elegant approach using events instead.
135 |
136 | Here is an (ugly) example of a get/set mutator setup for an intermediate table using a pivot field called `type`.
137 |
138 | ```php
139 | // model User
140 | class User ... {
141 | public function roles() {
142 | return $this->belongsToMany->using(RoleUser::class)->withPivot('rules_all');
143 | }
144 | }
145 |
146 | // model Role
147 | class Role ... {
148 | public function users() {
149 | return $this->belongsToMany->using(RoleUser::class)->withPivot('rules_all');
150 | }
151 | }
152 |
153 | // intermediate table
154 | use Illuminate\Database\Eloquent\Relations\Pivot;
155 | class RoleUser extends Pivot {
156 |
157 | protected $table 'role_user';
158 |
159 | public function getType1Attribute() {
160 | return $this->type;
161 | }
162 |
163 | public function setType1Attribute($value) {
164 | $this->attributes['type'] = $value;
165 | }
166 |
167 | // ... repeat for as many types as needed
168 | }
169 | ```
170 |
171 | And now for the dependency container.
172 |
173 | ```php
174 | ->fields(function() {
175 | return [
176 | DependencyContainer::make([
177 | // pivot field rules_all
178 | Select::make('Type', 'type_1')
179 | ->options([
180 | /* some options */
181 | ])
182 | ->displayUsingLabels()
183 | ])
184 | ->dependsOn('role_user', 1),
185 |
186 | DependencyContainer::make([
187 | // pivot field rules_all
188 | Select::make('Type', 'type_2')
189 | ->options([
190 | /* different options */
191 | ])
192 | ->displayUsingLabels()
193 | ])
194 | ->dependsOn('role_user', 2),
195 |
196 | // .. and so on
197 | ]
198 | }),
199 | ```
200 |
201 | ## MorphTo dependency
202 |
203 | A similar example taken from Novas documentation
204 | for [MorphTo](https://nova.laravel.com/docs/2.0/resources/relationships.html#morphto) is called commentable. It uses 3
205 | Models; `Comment`, `Video` and `Post`. Here `Comment` has the morphable fields `commentable_id` and `commentable_type`
206 |
207 | For a `MorphTo` dependency, the following construction is needed.
208 |
209 | `Commentable` becomes lower case `commentable` and the value to depend on is the resource singular form. In this example
210 | the dependency container will add two additional fields, `Additional Text` and `Visible`, only when the `Post` resource
211 | is selected.
212 |
213 | ```php
214 | MorphTo::make('Commentable')->types([
215 | Post::class,
216 | Video::class,
217 | ]),
218 |
219 | DependencyContainer::make([
220 | Text::make('Additional Text', 'additional'),
221 | Boolean::make('Visible', 'visible')
222 | ])
223 | ->dependsOn('commentable', 'Post')
224 | ```
225 |
226 | ## Workaround for index or details page
227 |
228 | Use the field within resource methods `fieldsForCreate` or `fieldsForUpdate`:
229 |
230 | ```php
231 | DependencyContainer::make([
232 | Select::make('Parent name', 'parent_id')
233 | ->options(...)
234 | ])->dependsOn('code', 'column'),
235 | ```
236 |
237 | To display some values on index or details page,
238 | use any field you like to display the value within resource methods `fieldsForIndex` or `fieldsForDetail`:
239 |
240 | ```php
241 | Select::make('Parent name', 'parent_id')
242 | ->options(...),
243 |
244 | // OR
245 |
246 | Text::make('Parent name', 'parent_id'),
247 | ```
248 |
249 | ## License
250 |
251 | The MIT License (MIT). Please
252 | see [License File](https://github.com/alexwenzel/nova-dependency-container/blob/master/LICENSE.md) for more information.
253 |
--------------------------------------------------------------------------------
/resources/js/components/DetailField.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
45 |
--------------------------------------------------------------------------------
/resources/js/components/FormField.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
174 |
--------------------------------------------------------------------------------
/resources/js/field.js:
--------------------------------------------------------------------------------
1 | import DetailField from './components/DetailField'
2 | import FormField from './components/FormField'
3 |
4 | Nova.booting((app, store) => {
5 | app.component('detail-dependency-container', DetailField)
6 | app.component('form-dependency-container', FormField)
7 | })
8 |
--------------------------------------------------------------------------------
/resources/js/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * walks a DOM node down
3 | * @param vnode
4 | * @param cb
5 | */
6 | export function walk(vnode, cb) {
7 | if (!vnode) return;
8 |
9 | if (vnode.component) {
10 | const proxy = vnode.component.proxy;
11 | if (proxy) cb(vnode.component.proxy);
12 | walk(vnode.component.subTree, cb);
13 | } else if (vnode.shapeFlag & 16) {
14 | const vnodes = vnode.children;
15 | for (let i = 0; i < vnodes.length; i++) {
16 | walk(vnodes[i], cb);
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/ActionHasDependencies.php:
--------------------------------------------------------------------------------
1 | fields() as $field) {
17 | if ($field instanceof DependencyContainer) {
18 | // do not add any fields for validation if container is not satisfied
19 | if ($field->areDependenciesSatisfied($request)) {
20 | $availableFields[] = $field;
21 | $this->extractChildFields($field->meta['fields']);
22 | }
23 | } else {
24 | $availableFields[] = $field;
25 | }
26 | }
27 |
28 | if ($this->childFieldsArr) {
29 | $availableFields = array_merge($availableFields, $this->childFieldsArr);
30 | }
31 | }
32 |
33 | /**
34 | * Validate action fields. Mostly a copy paste from Nova
35 | *
36 | * Uses the above to validate only on fields that have satisfied dependencies.
37 | *
38 | * @param \Laravel\Nova\Http\Requests\ActionRequest $request
39 | * @return array
40 | */
41 | public function validateFields(ActionRequest $request)
42 | {
43 | $fields = collect($this->fieldsForValidation($request));
44 |
45 | return Validator::make(
46 | $request->all(),
47 | $fields->mapWithKeys(function ($field) use ($request) {
48 | return $field->getCreationRules($request);
49 | })->all(),
50 | [],
51 | $fields->reject(function ($field) {
52 | return empty($field->name);
53 | })->mapWithKeys(function ($field) {
54 | return [$field->attribute => $field->name];
55 | })->all()
56 | )->after(function ($validator) use ($request) {
57 | $this->afterValidation($request, $validator);
58 | })->validate();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/DependencyContainer.php:
--------------------------------------------------------------------------------
1 | withMeta(['fields' => $fields]);
39 | $this->withMeta(['dependencies' => []]);
40 | }
41 |
42 | /**
43 | * Adds a dependency
44 | *
45 | * @param $field
46 | * @param $value
47 | * @return $this
48 | */
49 | public function dependsOn($field, $value)
50 | {
51 | return $this->withMeta([
52 | 'dependencies' => array_merge($this->meta['dependencies'], [
53 | $this->getFieldLayout($field, $value),
54 | ]),
55 | ]);
56 | }
57 |
58 | /**
59 | * Adds a dependency for not
60 | *
61 | * @param $field
62 | * @return DependencyContainer
63 | */
64 | public function dependsOnNot($field, $value)
65 | {
66 | return $this->withMeta([
67 | 'dependencies' => array_merge($this->meta['dependencies'], [
68 | array_merge($this->getFieldLayout($field), ['not' => $value]),
69 | ]),
70 | ]);
71 | }
72 |
73 | /**
74 | * Adds a dependency for not empty
75 | *
76 | * @param $field
77 | * @return DependencyContainer
78 | */
79 | public function dependsOnEmpty($field)
80 | {
81 | return $this->withMeta([
82 | 'dependencies' => array_merge($this->meta['dependencies'], [
83 | array_merge($this->getFieldLayout($field), ['empty' => true]),
84 | ]),
85 | ]);
86 | }
87 |
88 | /**
89 | * Adds a dependency for not empty
90 | *
91 | * @param $field
92 | * @return DependencyContainer
93 | */
94 | public function dependsOnNotEmpty($field)
95 | {
96 | return $this->withMeta([
97 | 'dependencies' => array_merge($this->meta['dependencies'], [
98 | array_merge($this->getFieldLayout($field), ['notEmpty' => true]),
99 | ]),
100 | ]);
101 | }
102 |
103 | /**
104 | * Adds a dependency for null or zero (0)
105 | *
106 | * @param $field
107 | * @param $value
108 | * @return $this
109 | */
110 | public function dependsOnNullOrZero($field)
111 | {
112 | return $this->withMeta([
113 | 'dependencies' => array_merge($this->meta['dependencies'], [
114 | array_merge($this->getFieldLayout($field), ['nullOrZero' => true]),
115 | ]),
116 | ]);
117 | }
118 |
119 | /**
120 | * Adds a dependency for in
121 | *
122 | * @param $field
123 | * @param $array
124 | * @return $this
125 | */
126 | public function dependsOnIn($field, $array)
127 | {
128 | return $this->withMeta([
129 | 'dependencies' => array_merge($this->meta['dependencies'], [
130 | array_merge($this->getFieldLayout($field), ['in' => $array]),
131 | ]),
132 | ]);
133 | }
134 |
135 | /**
136 | * Adds a dependency for not in
137 | *
138 | * @param $field
139 | * @param $array
140 | * @return $this
141 | */
142 | public function dependsOnNotIn($field, $array)
143 | {
144 | return $this->withMeta([
145 | 'dependencies' => array_merge($this->meta['dependencies'], [
146 | array_merge($this->getFieldLayout($field), ['notin' => $array]),
147 | ]),
148 | ]);
149 | }
150 |
151 | /**
152 | * Get layout for a specified field. Dot notation will result in {field}.{property}. If no dot was found it will
153 | * result in {field}.{field}, as it was in previous versions by default.
154 | *
155 | * @param $field
156 | * @param $value
157 | * @return array
158 | */
159 | protected function getFieldLayout($field, $value = null)
160 | {
161 | if (count(($field = explode('.', $field))) === 1) {
162 | // backwards compatibility, property becomes field
163 | $field[1] = $field[0];
164 | }
165 | return [
166 | // literal form input name
167 | 'field' => $field[0],
168 | // property to compare
169 | 'property' => $field[1],
170 | // value to compare
171 | 'value' => $value,
172 | ];
173 | }
174 |
175 | /**
176 | * Resolve dependency fields for display
177 | *
178 | * @param mixed $resource
179 | * @param null $attribute
180 | */
181 | public function resolveForDisplay($resource, $attribute = null)
182 | {
183 | foreach ($this->meta['fields'] as $field) {
184 | $field->resolveForDisplay($resource);
185 | }
186 |
187 | foreach ($this->meta['dependencies'] as $index => $dependency) {
188 |
189 | $this->meta['dependencies'][$index]['satisfied'] = false;
190 |
191 | if (array_key_exists('empty', $dependency) && empty($resource->{$dependency['property']})) {
192 | $this->meta['dependencies'][$index]['satisfied'] = true;
193 | continue;
194 | }
195 | // inverted `empty()`
196 | if (array_key_exists('notEmpty', $dependency) && !empty($resource->{$dependency['property']})) {
197 | $this->meta['dependencies'][$index]['satisfied'] = true;
198 | continue;
199 | }
200 | // inverted
201 | if (array_key_exists('nullOrZero', $dependency) && in_array($resource->{$dependency['property']},
202 | [null, 0, '0'], true)) {
203 | $this->meta['dependencies'][$index]['satisfied'] = true;
204 | continue;
205 | }
206 |
207 | if (array_key_exists('not', $dependency) && $resource->{$dependency['property']} != $dependency['not']) {
208 | $this->meta['dependencies'][$index]['satisfied'] = true;
209 | continue;
210 | }
211 |
212 | if (array_key_exists('in', $dependency) && in_array($resource->{$dependency['property']}, $dependency['in'])) {
213 | $this->meta['dependencies'][$index]['satisfied'] = true;
214 | continue;
215 | }
216 |
217 | if (array_key_exists('notin', $dependency) && !in_array($resource->{$dependency['property']}, $dependency['notin'])) {
218 | $this->meta['dependencies'][$index]['satisfied'] = true;
219 | continue;
220 | }
221 |
222 | if (array_key_exists('value', $dependency)) {
223 | if (is_array($resource)) {
224 | if (isset($resource[$dependency['property']]) && $dependency['value'] == $resource[$dependency['property']]) {
225 | $this->meta['dependencies'][$index]['satisfied'] = true;
226 | }
227 | continue;
228 | } elseif ($dependency['value'] == $resource->{$dependency['property']}) {
229 | $this->meta['dependencies'][$index]['satisfied'] = true;
230 | continue;
231 | }
232 | // @todo: quickfix for MorphTo
233 | $morphable_attribute = $resource->getAttribute($dependency['property'] . '_type');
234 | if ($morphable_attribute !== null && Str::endsWith($morphable_attribute, '\\' . $dependency['value'])) {
235 | $this->meta['dependencies'][$index]['satisfied'] = true;
236 | continue;
237 | }
238 | }
239 |
240 | }
241 | }
242 |
243 | /**
244 | * Resolve dependency fields
245 | *
246 | * @param mixed $resource
247 | * @param string $attribute
248 | * @return array|mixed
249 | */
250 | public function resolve($resource, $attribute = null)
251 | {
252 | foreach ($this->meta['fields'] as $field) {
253 | $field->resolve($resource, $attribute);
254 | }
255 | }
256 |
257 | /**
258 | * Forward fillInto request for each field in this container
259 | *
260 | * @trace fill/fillForAction -> fillInto -> *
261 | *
262 | * @param NovaRequest $request
263 | * @param $model
264 | * @param $attribute
265 | * @param null $requestAttribute
266 | */
267 | public function fillInto(NovaRequest $request, $model, $attribute, $requestAttribute = null)
268 | {
269 | $callbacks = [];
270 |
271 | foreach ($this->meta['fields'] as $field) {
272 | /** @var Field $field */
273 | $callbacks[] = $field->fill($request, $model);
274 | }
275 |
276 | return function () use ($callbacks) {
277 | foreach ($callbacks as $callback) {
278 | if (is_callable($callback)) {
279 | call_user_func($callback);
280 | }
281 | }
282 | };
283 | }
284 |
285 | /**
286 | * Checks whether to add validation rules
287 | *
288 | * @param NovaRequest $request
289 | * @return bool
290 | */
291 | public function areDependenciesSatisfied(NovaRequest $request)
292 | {
293 | if (!isset($this->meta['dependencies'])
294 | || !is_array($this->meta['dependencies'])) {
295 | return false;
296 | }
297 |
298 | $satisfiedCounts = 0;
299 | foreach ($this->meta['dependencies'] as $index => $dependency) {
300 |
301 | // dependsOnEmpty
302 | if (array_key_exists('empty', $dependency) && empty($request->has($dependency['property']))) {
303 | $satisfiedCounts++;
304 | }
305 |
306 | // dependsOnNotEmpty
307 | if (array_key_exists('notEmpty', $dependency) && !empty($request->has($dependency['property']))) {
308 | $satisfiedCounts++;
309 | }
310 |
311 | // dependsOnNullOrZero
312 | if (array_key_exists('nullOrZero', $dependency)
313 | && in_array($request->get($dependency['property']), [null, 0, '0', ''], true)) {
314 | $satisfiedCounts++;
315 | }
316 |
317 | // dependsOnIn
318 | if (array_key_exists('in', $dependency)
319 | && in_array($request->get($dependency['property']), $dependency['in'])) {
320 | $satisfiedCounts++;
321 | }
322 |
323 | // dependsOnNotIn
324 | if (array_key_exists('notin', $dependency)
325 | && !in_array($request->get($dependency['property']), $dependency['notin'])) {
326 | $satisfiedCounts++;
327 | }
328 |
329 | // dependsOnNot
330 | if (array_key_exists('not', $dependency) && $dependency['not'] != $request->get($dependency['property'])) {
331 | $satisfiedCounts++;
332 | }
333 |
334 | // dependsOn
335 | if (array_key_exists('value', $dependency)
336 | && !array_key_exists('in', $dependency)
337 | && !array_key_exists('notin', $dependency)
338 | && !array_key_exists('nullOrZero', $dependency)) {
339 | if ($dependency['value'] instanceof BackedEnum) {
340 | if ($dependency['value']->value == $request->get($dependency['property'])) {
341 | $satisfiedCounts++;
342 | }
343 | } elseif ($dependency['value'] == $request->get($dependency['property'])) {
344 | $satisfiedCounts++;
345 | }
346 | }
347 | }
348 |
349 | return $satisfiedCounts == count($this->meta['dependencies']);
350 | }
351 |
352 | /**
353 | * Get a rule set based on field property name
354 | *
355 | * @param NovaRequest $request
356 | * @param string $methodName
357 | * @return array
358 | */
359 | protected function getSituationalRulesSet(NovaRequest $request, string $methodName = 'getRules')
360 | {
361 | $fieldsRules = [$this->attribute => []];
362 |
363 | // if dependencies are not satisfied
364 | // or no fields as dependency exist
365 | // return empty rules for dependency container
366 | if (!$this->areDependenciesSatisfied($request)
367 | || !isset($this->meta['fields'])
368 | || !is_array($this->meta['fields'])) {
369 | return $fieldsRules;
370 | }
371 |
372 | /** @var Field $field */
373 | foreach ($this->meta['fields'] as $field) {
374 | // if field is DependencyContainer, then add rules from dependant fields
375 | if ($field instanceof DependencyContainer && $methodName === "getRules") {
376 | $fieldsRules[Str::random()] = $field->getSituationalRulesSet($request, $methodName);
377 | } elseif ($field instanceof Medialibrary) {
378 | $rules = $field->{$methodName}($request);
379 |
380 | $fieldsRules[$field->attribute] = MediaCollectionRules::make(
381 | $rules,
382 | $request,
383 | $field,
384 | );
385 | } else {
386 | $fieldsRules[$field->attribute] = $field->{$methodName}($request);
387 | }
388 | }
389 |
390 | // simplify nested rules to one level
391 | return $this->array_simplify($fieldsRules);
392 | }
393 |
394 | /**
395 | * @param $array
396 | * @return array
397 | */
398 | protected function array_simplify($array): array
399 | {
400 | $result = [];
401 |
402 | foreach ($array as $key => $value) {
403 | if (is_string($key) && is_array($value) && !empty($value)) {
404 | if (count(array_filter(array_keys($value), 'is_string')) > 0) {
405 | $result = array_merge($result, $this->array_simplify($value));
406 | } else {
407 | $result[$key] = $value;
408 | }
409 | }
410 | }
411 |
412 | return $result;
413 | }
414 |
415 | /**
416 | * Get the validation rules for this field.
417 | *
418 | * @param NovaRequest $request
419 | * @return array
420 | */
421 | public function getRules(NovaRequest $request)
422 | {
423 | return $this->getSituationalRulesSet($request);
424 | }
425 |
426 | /**
427 | * Get the creation rules for this field.
428 | *
429 | * @param NovaRequest $request
430 | * @return array|string
431 | */
432 | public function getCreationRules(NovaRequest $request)
433 | {
434 | $fieldsRules = $this->getSituationalRulesSet($request, 'getCreationRules');
435 |
436 | return array_merge_recursive(
437 | $this->getRules($request),
438 | $fieldsRules
439 | );
440 | }
441 |
442 | /**
443 | * Get the update rules for this field.
444 | *
445 | * @param NovaRequest $request
446 | * @return array
447 | */
448 | public function getUpdateRules(NovaRequest $request)
449 | {
450 | $fieldsRules = $this->getSituationalRulesSet($request, 'getUpdateRules');
451 |
452 | return array_merge_recursive(
453 | $this->getRules($request),
454 | $fieldsRules
455 | );
456 | }
457 | }
458 |
--------------------------------------------------------------------------------
/src/FieldServiceProvider.php:
--------------------------------------------------------------------------------
1 | bind(
29 | \Laravel\Nova\Http\Controllers\ActionController::class,
30 | \Alexwenzel\DependencyContainer\Http\Controllers\ActionController::class
31 | );
32 | });
33 | }
34 |
35 | /**
36 | * Register any application services.
37 | *
38 | * @return void
39 | */
40 | public function register(): void
41 | {
42 | //
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/HasChildFields.php:
--------------------------------------------------------------------------------
1 | extractChildFields($childField->meta['fields']);
18 | } else {
19 | if (array_search($childField->attribute, array_column($this->childFieldsArr, 'attribute')) === false) {
20 | // @todo: we should not randomly apply rules to child-fields.
21 | $childField = $this->applyRulesForChildFields($childField);
22 | $this->childFieldsArr[] = $childField;
23 | }
24 | }
25 | }
26 | }
27 |
28 | /**
29 | * @param [array] $childField
30 | * @return [array] $childField
31 | */
32 | protected function applyRulesForChildFields($childField)
33 | {
34 | if (isset($childField->rules)) {
35 | $childField->rules[] = "sometimes:required:".$childField->attribute;
36 | }
37 | if (isset($childField->creationRules)) {
38 | $childField->creationRules[] = "sometimes:required:".$childField->attribute;
39 | }
40 | if (isset($childField->updateRules)) {
41 | $childField->updateRules[] = "sometimes:required:".$childField->attribute;
42 | }
43 | return $childField;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/HasDependencies.php:
--------------------------------------------------------------------------------
1 | fieldsMethod($request);
21 |
22 | // needs to be filtered once to resolve Panels
23 | $fields = $this->filter($this->{$method}($request));
24 | $availableFields = [];
25 |
26 | foreach ($fields as $field) {
27 | if ($field instanceof DependencyContainer) {
28 | $availableFields[] = $this->filterFieldForRequest($field, $request);
29 | if ($field->areDependenciesSatisfied($request) || $this->extractableRequest($request, $this->model())) {
30 | if ($this->doesRouteRequireChildFields()) {
31 | $this->extractChildFields($field->meta['fields']);
32 | }
33 | }
34 | } else {
35 | $availableFields[] = $this->filterFieldForRequest($field, $request);
36 | }
37 | }
38 |
39 | if ($this->childFieldsArr) {
40 | $availableFields = array_merge($availableFields, $this->childFieldsArr);
41 | }
42 |
43 | $availableFields = new FieldCollection(array_values($this->filter($availableFields)));
44 |
45 | return $availableFields;
46 | }
47 |
48 | /**
49 | * Check if request needs to extract child fields
50 | *
51 | * @param NovaRequest $request
52 | * @param mixed $model
53 | * @return bool
54 | */
55 | protected function extractableRequest(NovaRequest $request, $model)
56 | {
57 | // if form was submitted to update (method === 'PUT')
58 | if ($request->isUpdateOrUpdateAttachedRequest() && $request->method() == 'PUT') {
59 | return false;
60 | }
61 |
62 | // if form was submitted to create and new resource
63 | if ($request->isCreateOrAttachRequest() && $model->id === null) {
64 | return false;
65 | }
66 |
67 | return true;
68 | }
69 |
70 | /**
71 | * @param mixed $field
72 | * @param NovaRequest $request
73 | * @return mixed
74 | *
75 | * @todo: implement
76 | */
77 | public function filterFieldForRequest($field, NovaRequest $request)
78 | {
79 | // @todo: filter fields for request, e.g. show/hideOnIndex, create, update or whatever
80 | return $field;
81 | }
82 |
83 | /**
84 | * @return bool
85 | */
86 | protected function doesRouteRequireChildFields(): bool
87 | {
88 | return Str::endsWith(Route::currentRouteAction(), [
89 | 'FieldDestroyController@handle',
90 | 'ResourceUpdateController@handle',
91 | 'ResourceStoreController@handle',
92 | 'AssociatableController@index',
93 | 'MorphableController@index',
94 | ]);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Http/Controllers/ActionController.php:
--------------------------------------------------------------------------------
1 | action();
21 |
22 | if (in_array(ActionHasDependencies::class, class_uses_recursive($action))) {
23 | $request = ActionRequest::createFrom($request);
24 | }
25 |
26 | return parent::store($request);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Http/Requests/ActionRequest.php:
--------------------------------------------------------------------------------
1 | action()->fields($this) as $field) {
25 | if ($field instanceof DependencyContainer) {
26 | // do not add any fields for validation if container is not satisfied
27 | if ($field->areDependenciesSatisfied($this)) {
28 | $availableFields[] = $field;
29 | $this->extractChildFields($field->meta['fields']);
30 | }
31 | } else {
32 | $availableFields[] = $field;
33 | }
34 | }
35 |
36 | if ($this->childFieldsArr) {
37 | $availableFields = array_merge($availableFields, $this->childFieldsArr);
38 | }
39 |
40 | $this->validate(collect($availableFields)->mapWithKeys(function ($field) {
41 | return $field->getCreationRules($this);
42 | })->all());
43 | }
44 |
45 | public function novaRequest() {
46 | return new NovaRequest;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/webpack.mix.js:
--------------------------------------------------------------------------------
1 | let mix = require('laravel-mix')
2 | let path = require('path')
3 |
4 | require('./nova.mix')
5 |
6 | mix
7 | .setPublicPath('dist')
8 | .js('resources/js/field.js', 'js')
9 | .vue({ version: 3 })
10 | .nova('alexwenzel/dependency-container')
11 |
--------------------------------------------------------------------------------