├── .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 | ![Demo](https://raw.githubusercontent.com/alexwenzel/nova-dependency-container/master/docs/demo.gif) 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 | ![Demo](https://raw.githubusercontent.com/alexwenzel/nova-dependency-container/master/docs/demo-2.gif) 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 | 14 | 15 | 45 | -------------------------------------------------------------------------------- /resources/js/components/FormField.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------