├── .gitattributes ├── .github └── workflows │ ├── pull_request.yml │ └── versioning.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── config └── asseco-custom-fields.php ├── erd.png ├── migrations ├── 2020_05_26_053145_create_custom_field_plain_types_table.php ├── 2020_05_26_053155_create_custom_field_remote_types_table.php ├── 2020_05_26_053165_create_custom_field_selection_types_table.php ├── 2020_05_26_063145_create_custom_field_selection_values_table.php ├── 2020_05_26_064150_create_custom_field_validations_table.php ├── 2020_05_26_064234_create_custom_fields_table.php ├── 2020_05_27_103953_create_custom_field_relations_table.php ├── 2020_06_01_103953_create_custom_field_values_table.php ├── 2020_06_02_124431_create_forms_table.php ├── 2020_06_02_174431_create_custom_field_form_table.php ├── 2021_09_13_143227_add_hidden_to_custom_fields_table.php ├── 2022_02_01_143227_create_form_templates_table.php ├── 2022_04_08_102613_add_unique_index_to_custom_field_selection_values_table.php ├── 2022_11_16_081022_update_custom_field_remote_types_table.php ├── 2023_03_29_095342_drop_unique_index_from_custom_fields_table.php ├── 2024_11_12_073145_add_json_type_to_custom_field_plain_types_table.php ├── 2024_11_12_073245_add_json_to_custom_field_values_table.php └── 2025_04_24_103227_add_renderer_to_custom_fields_table.php ├── phpunit.xml ├── routes └── api.php ├── src ├── App │ ├── Contracts │ │ ├── CustomField.php │ │ ├── Form.php │ │ ├── FormTemplate.php │ │ ├── Mappable.php │ │ ├── PlainType.php │ │ ├── PlainTypes │ │ │ ├── BooleanType.php │ │ │ ├── DateTimeType.php │ │ │ ├── DateType.php │ │ │ ├── FloatType.php │ │ │ ├── IntegerType.php │ │ │ ├── JsonType.php │ │ │ ├── StringType.php │ │ │ ├── TextType.php │ │ │ └── TimeType.php │ │ ├── Relation.php │ │ ├── RemoteType.php │ │ ├── SelectionType.php │ │ ├── SelectionValue.php │ │ ├── Validation.php │ │ └── Value.php │ ├── Exceptions │ │ ├── FieldValidationException.php │ │ ├── Handler.php │ │ └── MissingRequiredFieldException.php │ ├── Http │ │ ├── Controllers │ │ │ ├── Controller.php │ │ │ ├── CustomFieldController.php │ │ │ ├── FormController.php │ │ │ ├── FormTemplateController.php │ │ │ ├── ModelController.php │ │ │ ├── PlainCustomFieldController.php │ │ │ ├── RelationController.php │ │ │ ├── RemoteCustomFieldController.php │ │ │ ├── SelectionCustomFieldController.php │ │ │ ├── SelectionValueController.php │ │ │ ├── TypeController.php │ │ │ ├── ValidationController.php │ │ │ └── ValueController.php │ │ └── Requests │ │ │ ├── CustomFieldCreateRequest.php │ │ │ ├── CustomFieldUpdateRequest.php │ │ │ ├── FormRequest.php │ │ │ ├── FormTemplateRequest.php │ │ │ ├── PlainCustomFieldRequest.php │ │ │ ├── RelationRequest.php │ │ │ ├── RemoteCustomFieldRequest.php │ │ │ ├── RemoteTypeRequest.php │ │ │ ├── SelectionCustomFieldRequest.php │ │ │ ├── SelectionTypeRequest.php │ │ │ ├── SelectionValueRequest.php │ │ │ ├── ValidationRequest.php │ │ │ └── ValueRequest.php │ ├── Models │ │ ├── CustomField.php │ │ ├── Form.php │ │ ├── FormTemplate.php │ │ ├── ParentType.php │ │ ├── PlainType.php │ │ ├── Relation.php │ │ ├── RemoteType.php │ │ ├── SelectionType.php │ │ ├── SelectionValue.php │ │ ├── Validation.php │ │ └── Value.php │ ├── PlainTypes │ │ ├── BooleanType.php │ │ ├── DateTimeType.php │ │ ├── DateType.php │ │ ├── FloatType.php │ │ ├── IntegerType.php │ │ ├── JsonType.php │ │ ├── StringType.php │ │ ├── TextType.php │ │ └── TimeType.php │ └── Traits │ │ ├── Customizable.php │ │ ├── FakesTypeValues.php │ │ ├── FindsTraits.php │ │ └── TransformsOutput.php ├── CustomFieldsServiceProvider.php └── Database │ ├── Factories │ ├── CustomFieldFactory.php │ ├── FormFactory.php │ ├── PlainTypeFactory.php │ ├── RelationFactory.php │ ├── RemoteTypeFactory.php │ ├── SelectionTypeFactory.php │ ├── SelectionValueFactory.php │ ├── ValidationFactory.php │ └── ValueFactory.php │ └── Seeders │ ├── CustomFieldFormSeeder.php │ ├── CustomFieldPackageSeeder.php │ ├── CustomFieldSeeder.php │ ├── FormSeeder.php │ ├── PlainTypeSeeder.php │ ├── RelationSeeder.php │ ├── RemoteTypeSeeder.php │ ├── SelectionTypeSeeder.php │ ├── SelectionValueSeeder.php │ ├── ValidationSeeder.php │ └── ValueSeeder.php └── tests ├── Feature └── Http │ └── Controllers │ ├── CustomFieldControllerTest.php │ ├── FormControllerTest.php │ ├── PlainCustomFieldControllerTest.php │ ├── RelationControllerTest.php │ ├── RemoteCustomFieldControllerTest.php │ ├── SelectionCustomFieldControllerTest.php │ ├── SelectionValueControllerTest.php │ ├── ValidationControllerTest.php │ └── ValueControllerTest.php ├── TestCase.php └── Unit └── Models ├── CustomFieldTest.php ├── FormTest.php ├── PlainTypeTest.php ├── RelationTest.php ├── RemoteTypeTest.php ├── SelectionTypeTest.php ├── SelectionValueTest.php ├── ValidationTest.php └── ValueTest.php /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PR pipeline 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-24.04 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup PHP 16 | uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: '8.2' 19 | extensions: mbstring, intl 20 | ini-values: post_max_size=256M, max_execution_time=180 21 | coverage: xdebug 22 | tools: php-cs-fixer, phpunit 23 | 24 | - name: Validate composer.json and composer.lock 25 | run: composer validate 26 | 27 | - name: Cache Composer packages 28 | id: composer-cache 29 | uses: actions/cache@v4 30 | with: 31 | path: vendor 32 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 33 | restore-keys: | 34 | ${{ runner.os }}-php- 35 | 36 | - name: Install dependencies 37 | if: steps.composer-cache.outputs.cache-hit != 'true' 38 | run: composer install --prefer-dist --no-progress --no-suggest 39 | 40 | - name: Execute PHPUnit tests 41 | run: 42 | vendor/phpunit/phpunit/phpunit 43 | 44 | - name: PHP STatic ANalyser (phpstan) 45 | uses: php-actions/phpstan@v3 46 | with: 47 | version: latest 48 | path: 'src' 49 | php_version: '8.2' 50 | level: 0 51 | -------------------------------------------------------------------------------- /.github/workflows/versioning.yml: -------------------------------------------------------------------------------- 1 | # Manual Bumping: Any PR title or commit message that includes #major, #minor, or #patch 2 | # will trigger the respective version bump. If two or more are present, 3 | # the highest-ranking one will take precedence. 4 | 5 | name: Bump version 6 | on: 7 | push: 8 | branches: 9 | - master 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: '0' 17 | - name: Bump version and push tag 18 | uses: anothrNick/github-tag-action@1.39.0 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | WITH_V: true 22 | DEFAULT_BUMP: none 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/hot 3 | /public/storage 4 | /storage/*.key 5 | /vendor 6 | .env 7 | .env.backup 8 | .phpunit.result.cache 9 | Homestead.json 10 | Homestead.yaml 11 | npm-debug.log 12 | yarn-error.log 13 | /.vscode 14 | /nbproject 15 | /.vagrant 16 | *.log 17 | \.php_cs\.cache 18 | 19 | .DS_Store 20 | .idea 21 | 22 | # User-specific stuff 23 | .idea/**/workspace.xml 24 | .idea/**/tasks.xml 25 | .idea/**/usage.statistics.xml 26 | .idea/**/dictionaries 27 | .idea/**/shelf 28 | 29 | # Generated files 30 | .idea/**/contentModel.xml 31 | 32 | # Sensitive or high-churn files 33 | .idea/**/dataSources/ 34 | .idea/**/dataSources.ids 35 | .idea/**/dataSources.local.xml 36 | .idea/**/sqlDataSources.xml 37 | .idea/**/dynamic.xml 38 | .idea/**/uiDesigner.xml 39 | .idea/**/dbnavigator.xml 40 | 41 | # Gradle 42 | .idea/**/gradle.xml 43 | .idea/**/libraries 44 | 45 | # Gradle and Maven with auto-import 46 | # When using Gradle or Maven with auto-import, you should exclude module files, 47 | # since they will be recreated, and may cause churn. Uncomment if using 48 | # auto-import. 49 | # .idea/modules.xml 50 | # .idea/*.iml 51 | # .idea/modules 52 | # *.iml 53 | # *.ipr 54 | 55 | # CMake 56 | cmake-build-*/ 57 | 58 | # Mongo Explorer plugin 59 | .idea/**/mongoSettings.xml 60 | 61 | # File-based project format 62 | *.iws 63 | 64 | # IntelliJ 65 | out/ 66 | 67 | # mpeltonen/sbt-idea plugin 68 | .idea_modules/ 69 | 70 | # JIRA plugin 71 | atlassian-ide-plugin.xml 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Asseco SEE 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": "asseco-voice/laravel-custom-fields", 3 | "description": "Laravel support for custom fields", 4 | "license": "MIT", 5 | "require": { 6 | "php": "^8.1", 7 | "laravel/framework": "^10.0", 8 | "asseco-voice/laravel-common": "^3.0" 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "^10.0", 12 | "mockery/mockery": "^1.4.4", 13 | "fakerphp/faker": "^1.9.1", 14 | "orchestra/testbench": "^8.5", 15 | "doctrine/dbal": "^3.0" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Asseco\\CustomFields\\": "src/" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "Asseco\\CustomFields\\Tests\\": "tests/" 25 | } 26 | }, 27 | "extra": { 28 | "laravel": { 29 | "providers": [ 30 | "Asseco\\CustomFields\\CustomFieldsServiceProvider" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config/asseco-custom-fields.php: -------------------------------------------------------------------------------- 1 | [ 31 | 'custom_field' => CustomField::class, 32 | 'form' => Form::class, 33 | 'form_template' => FormTemplate::class, 34 | 'plain_type' => PlainType::class, 35 | 'relation' => Relation::class, 36 | 'remote_type' => RemoteType::class, 37 | 'selection_type' => SelectionType::class, 38 | 'selection_value' => SelectionValue::class, 39 | 'validation' => Validation::class, 40 | 'value' => Value::class, 41 | ], 42 | 43 | 'plain_types' => [ 44 | 'boolean' => BooleanType::class, 45 | 'datetime' => DateTimeType::class, 46 | 'date' => DateType::class, 47 | 'float' => FloatType::class, 48 | 'integer' => IntegerType::class, 49 | 'string' => StringType::class, 50 | 'text' => TextType::class, 51 | 'time' => TimeType::class, 52 | 'json' => JsonType::class, 53 | ], 54 | 55 | 'migrations' => [ 56 | 57 | /** 58 | * UUIDs as primary keys. 59 | */ 60 | 'uuid' => false, 61 | 62 | /** 63 | * Timestamp types. 64 | * 65 | * @see https://github.com/asseco-voice/laravel-common/blob/master/config/asseco-common.php 66 | */ 67 | 'timestamps' => MigrationMethodPicker::PLAIN, 68 | 69 | /** 70 | * Should the package run the migrations. Set to false if you're publishing 71 | * and changing default migrations. 72 | */ 73 | 'run' => true, 74 | ], 75 | 76 | /** 77 | * Path to Laravel models in 'path => namespace' format. 78 | * 79 | * This does not recurse in folders, so you need to specify 80 | * an array of paths if non-standard models are to be used 81 | */ 82 | 'models_path' => [ 83 | app_path('Models') => 'App\\Models\\', 84 | ], 85 | 86 | /** 87 | * Namespace to Customizable trait. 88 | */ 89 | 'trait_path' => Customizable::class, 90 | 91 | 'routes' => [ 92 | 'prefix' => 'api', 93 | 'middleware' => ['api'], 94 | ], 95 | 96 | /** 97 | * Determines if the response from resolving a remote custom field should be cached. 98 | */ 99 | 'should_cache_remote' => false, 100 | 101 | /** 102 | * Number of seconds that the remote custom field response should remain in cache. 103 | */ 104 | 'remote_cache_ttl' => 3600, 105 | ]; 106 | -------------------------------------------------------------------------------- /erd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asseco-voice/laravel-custom-fields/997b2cf70c0ff892df8a54a1862fd837c1be1ffd/erd.png -------------------------------------------------------------------------------- /migrations/2020_05_26_053145_create_custom_field_plain_types_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 22 | } else { 23 | $table->id(); 24 | } 25 | 26 | $table->string('name', 150)->unique('cf_name_types'); 27 | 28 | MigrationMethodPicker::pick($table, config('asseco-custom-fields.migrations.timestamps')); 29 | }); 30 | 31 | $this->seedData(); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::dropIfExists('custom_field_plain_types'); 42 | } 43 | 44 | protected function seedData(): void 45 | { 46 | $types = array_keys(config('asseco-custom-fields.plain_types')); 47 | 48 | $plainTypes = []; 49 | foreach ($types as $type) { 50 | if (config('asseco-custom-fields.migrations.uuid')) { 51 | $plainTypes[] = [ 52 | 'id' => Str::uuid(), 53 | 'name' => $type, 54 | 'created_at' => now(), 55 | 'updated_at' => now(), 56 | ]; 57 | } else { 58 | $plainTypes[] = [ 59 | 'name' => $type, 60 | 'created_at' => now(), 61 | 'updated_at' => now(), 62 | ]; 63 | } 64 | } 65 | 66 | DB::table('custom_field_plain_types')->insert($plainTypes); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /migrations/2020_05_26_053155_create_custom_field_remote_types_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 20 | $table->foreignUuid('plain_type_id')->constrained('custom_field_plain_types'); 21 | } else { 22 | $table->id(); 23 | $table->foreignId('plain_type_id')->constrained('custom_field_plain_types'); 24 | } 25 | 26 | $table->string('url'); 27 | $table->enum('method', ['GET', 'POST', 'PUT']); 28 | $table->json('body')->nullable(); 29 | $table->json('headers')->nullable(); 30 | $table->json('mappings')->nullable(); 31 | 32 | MigrationMethodPicker::pick($table, config('asseco-custom-fields.migrations.timestamps')); 33 | }); 34 | } 35 | 36 | /** 37 | * Reverse the migrations. 38 | * 39 | * @return void 40 | */ 41 | public function down() 42 | { 43 | Schema::dropIfExists('custom_field_remote_types'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /migrations/2020_05_26_053165_create_custom_field_selection_types_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 20 | $table->foreignUuid('plain_type_id')->constrained('custom_field_plain_types'); 21 | } else { 22 | $table->id(); 23 | $table->foreignId('plain_type_id')->constrained('custom_field_plain_types'); 24 | } 25 | 26 | $table->boolean('multiselect')->default(false); 27 | 28 | MigrationMethodPicker::pick($table, config('asseco-custom-fields.migrations.timestamps')); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists('custom_field_selection_types'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /migrations/2020_05_26_063145_create_custom_field_selection_values_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 20 | $table->foreignUuid('selection_type_id')->constrained('custom_field_selection_types')->cascadeOnDelete(); 21 | } else { 22 | $table->id(); 23 | $table->foreignId('selection_type_id')->constrained('custom_field_selection_types')->cascadeOnDelete(); 24 | } 25 | 26 | $table->string('label')->nullable(); 27 | $table->string('value'); 28 | $table->boolean('preselect')->default(false); 29 | 30 | MigrationMethodPicker::pick($table, config('asseco-custom-fields.migrations.timestamps')); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::dropIfExists('custom_field_selection_values'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /migrations/2020_05_26_064150_create_custom_field_validations_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 20 | } else { 21 | $table->id(); 22 | } 23 | 24 | $table->string('name', 150)->unique('cf_name_validations'); 25 | $table->string('regex', 255)->nullable(); 26 | 27 | // Set to true for predefined validations which should be shown on frontend dropdown. 28 | $table->boolean('generic')->default(false); 29 | 30 | MigrationMethodPicker::pick($table, config('asseco-custom-fields.migrations.timestamps')); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::dropIfExists('custom_field_validations'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /migrations/2020_05_26_064234_create_custom_fields_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 20 | $table->uuidMorphs('selectable'); 21 | $table->foreignUuid('validation_id')->nullable()->constrained('custom_field_validations')->nullOnDelete(); 22 | } else { 23 | $table->id(); 24 | $table->morphs('selectable'); 25 | $table->foreignId('validation_id')->nullable()->constrained('custom_field_validations')->nullOnDelete(); 26 | } 27 | 28 | $table->string('name')->unique('cf_name'); 29 | $table->string('label', 255); 30 | $table->string('placeholder')->nullable(); 31 | $table->string('model'); 32 | $table->boolean('required')->default(0); 33 | $table->string('group')->nullable(); 34 | $table->integer('order')->nullable(); 35 | 36 | MigrationMethodPicker::pick($table, config('asseco-custom-fields.migrations.timestamps')); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | * 43 | * @return void 44 | */ 45 | public function down() 46 | { 47 | Schema::dropIfExists('custom_fields'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /migrations/2020_05_27_103953_create_custom_field_relations_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 20 | $table->foreignUuid('parent_id')->constrained('custom_fields'); 21 | $table->foreignUuid('child_id')->constrained('custom_fields'); 22 | } else { 23 | $table->id(); 24 | $table->foreignId('parent_id')->constrained('custom_fields'); 25 | $table->foreignId('child_id')->constrained('custom_fields'); 26 | } 27 | 28 | MigrationMethodPicker::pick($table, config('asseco-custom-fields.migrations.timestamps')); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists('custom_field_relations'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /migrations/2020_06_01_103953_create_custom_field_values_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 20 | $table->foreignUuid('custom_field_id')->constrained()->cascadeOnDelete(); 21 | $table->uuidMorphs('model'); 22 | } else { 23 | $table->id(); 24 | $table->foreignId('custom_field_id')->constrained()->cascadeOnDelete(); 25 | $table->morphs('model'); 26 | } 27 | 28 | $table->string('string', 255)->nullable(); 29 | $table->integer('integer')->nullable(); 30 | $table->float('float', 18, 6)->nullable(); 31 | $table->text('text')->nullable(); 32 | $table->boolean('boolean')->nullable(); 33 | $table->datetime('datetime')->nullable(); 34 | $table->date('date')->nullable(); 35 | $table->time('time')->nullable(); 36 | 37 | MigrationMethodPicker::pick($table, config('asseco-custom-fields.migrations.timestamps')); 38 | }); 39 | } 40 | 41 | /** 42 | * Reverse the migrations. 43 | * 44 | * @return void 45 | */ 46 | public function down() 47 | { 48 | Schema::dropIfExists('custom_field_values'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /migrations/2020_06_02_124431_create_forms_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 20 | } else { 21 | $table->id(); 22 | } 23 | 24 | $table->string('tenant_id', 30)->nullable(); 25 | $table->string('name')->unique('form_name'); 26 | $table->json('definition'); 27 | $table->string('action_url')->nullable(); 28 | 29 | MigrationMethodPicker::pick($table, config('asseco-custom-fields.migrations.timestamps')); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::dropIfExists('forms'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /migrations/2020_06_02_174431_create_custom_field_form_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | 19 | if (config('asseco-custom-fields.migrations.uuid')) { 20 | $table->foreignUuid('custom_field_id')->constrained()->cascadeOnDelete(); 21 | $table->foreignUuid('form_id')->constrained()->cascadeOnDelete(); 22 | } else { 23 | $table->foreignId('custom_field_id')->constrained()->cascadeOnDelete(); 24 | $table->foreignId('form_id')->constrained()->cascadeOnDelete(); 25 | } 26 | 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('custom_field_form'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /migrations/2021_09_13_143227_add_hidden_to_custom_fields_table.php: -------------------------------------------------------------------------------- 1 | boolean('hidden')->default(false); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('custom_fields', function (Blueprint $table) { 29 | $table->dropColumn('hidden'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /migrations/2022_02_01_143227_create_form_templates_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 20 | $table->foreignUuid('form_id')->constrained(); 21 | } else { 22 | $table->id(); 23 | $table->foreignId('form_id')->constrained(); 24 | } 25 | 26 | $table->string('name'); 27 | MigrationMethodPicker::pick($table, config('asseco-custom-fields.migrations.timestamps')); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::table('form_templates', function (Blueprint $table) { 39 | $table->dropIfExists('form_templates'); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /migrations/2022_04_08_102613_add_unique_index_to_custom_field_selection_values_table.php: -------------------------------------------------------------------------------- 1 | consolidate(); 18 | 19 | Schema::table('custom_field_selection_values', function (Blueprint $table) { 20 | $table->unique(['selection_type_id', 'value']); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::table('custom_field_selection_values', function (Blueprint $table) { 32 | $table->dropForeign(['selection_type_id']); 33 | }); 34 | 35 | Schema::table('custom_field_selection_values', function (Blueprint $table) { 36 | $table->dropUnique(['selection_type_id', 'value']); 37 | }); 38 | 39 | Schema::table('custom_field_selection_values', function (Blueprint $table) { 40 | $table 41 | ->foreign('selection_type_id') 42 | ->references('id') 43 | ->on('custom_field_selection_types') 44 | ->cascadeOnDelete(); 45 | }); 46 | } 47 | 48 | protected function consolidate() 49 | { 50 | $selectionValues = DB::table('custom_field_selection_values')->get(); 51 | 52 | foreach ($selectionValues as $selectionValue) { 53 | if (DB::table('custom_field_selection_values')->where('id', $selectionValue->id)->exists()) { 54 | $ids = DB::table('custom_field_selection_values') 55 | ->where('selection_type_id', $selectionValue->selection_type_id) 56 | ->where('value', $selectionValue->value) 57 | ->where('id', '!=', $selectionValue->id) 58 | ->pluck('id'); 59 | 60 | DB::table('custom_field_selection_values')->whereIn('id', $ids)->delete(); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /migrations/2022_11_16_081022_update_custom_field_remote_types_table.php: -------------------------------------------------------------------------------- 1 | string('data_path')->nullable()->after('mappings'); 18 | $table->string('identifier_property')->nullable()->after('mappings'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('custom_field_remote_types', function (Blueprint $table) { 30 | $table->dropColumn('data_path'); 31 | }); 32 | 33 | Schema::table('custom_field_remote_types', function (Blueprint $table) { 34 | $table->dropColumn('identifier_property'); 35 | }); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /migrations/2023_03_29_095342_drop_unique_index_from_custom_fields_table.php: -------------------------------------------------------------------------------- 1 | dropUnique('cf_name'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('custom_fields', function (Blueprint $table) { 29 | $table->unique('name', 'cf_name'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /migrations/2024_11_12_073145_add_json_type_to_custom_field_plain_types_table.php: -------------------------------------------------------------------------------- 1 | where('name', 'json')->exists(); 17 | if ($exists) { 18 | // already exists 19 | return; 20 | } 21 | 22 | $types = ['json']; 23 | 24 | $plainTypes = []; 25 | foreach ($types as $type) { 26 | if (config('asseco-custom-fields.migrations.uuid')) { 27 | $plainTypes[] = [ 28 | 'id' => Str::uuid(), 29 | 'name' => $type, 30 | 'created_at' => now(), 31 | 'updated_at' => now(), 32 | ]; 33 | } else { 34 | $plainTypes[] = [ 35 | 'name' => $type, 36 | 'created_at' => now(), 37 | 'updated_at' => now(), 38 | ]; 39 | } 40 | } 41 | 42 | DB::table('custom_field_plain_types')->insert($plainTypes); 43 | } 44 | 45 | /** 46 | * Reverse the migrations. 47 | * 48 | * @return void 49 | */ 50 | public function down() 51 | { 52 | DB::table('custom_field_plain_types') 53 | ->where('name', 'json') 54 | ->delete(); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /migrations/2024_11_12_073245_add_json_to_custom_field_values_table.php: -------------------------------------------------------------------------------- 1 | json('json')->nullable()->default(null)->after('time'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('custom_field_values', function (Blueprint $table) { 29 | $table->dropColumn('json'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /migrations/2025_04_24_103227_add_renderer_to_custom_fields_table.php: -------------------------------------------------------------------------------- 1 | text('renderer')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('custom_fields', function (Blueprint $table) { 29 | $table->dropColumn('renderer'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/Feature 6 | 7 | 8 | ./tests/Unit 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ./app 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | middleware(config('asseco-custom-fields.routes.middleware')) 36 | ->group(function () { 37 | Route::apiResource('custom-fields', CustomFieldController::class); 38 | 39 | Route::prefix('custom-field')->name('custom-field.')->group(function () { 40 | Route::get('types', [TypeController::class, 'index'])->name('types.index'); 41 | Route::get('models', [ModelController::class, 'index'])->name('models.index'); 42 | 43 | Route::get('plain/{plain_type?}', [PlainCustomFieldController::class, 'index'])->name('plain.index'); 44 | Route::post('plain/{plain_type}', [PlainCustomFieldController::class, 'store'])->name('plain.store'); 45 | 46 | Route::apiResource('remote', RemoteCustomFieldController::class)->only(['index', 'store']); 47 | Route::match(['put', 'patch'], 'remote/{remote_type}', [RemoteCustomFieldController::class, 'update'])->name('remote.update'); 48 | Route::get('remote/{remote_type}/resolve', [RemoteCustomFieldController::class, 'resolve'])->name('remote.resolve'); 49 | Route::get('remote/{remote_type}/resolve/{identifier_value}', [RemoteCustomFieldController::class, 'resolveByIdentifierValue'])->name('remote.resolveByIdentifierValue'); 50 | 51 | Route::get('selection', [SelectionCustomFieldController::class, 'index'])->name('selection.index'); 52 | Route::post('selection/{plain_type}', [SelectionCustomFieldController::class, 'store'])->name('selection.store'); 53 | Route::match(['put', 'patch'], 'selection/{selection_type}', [SelectionCustomFieldController::class, 'update'])->name('selection.update'); 54 | 55 | Route::apiResource('selection-values', SelectionValueController::class); 56 | 57 | Route::apiResource('validations', ValidationController::class); 58 | Route::apiResource('relations', RelationController::class); 59 | Route::apiResource('values', ValueController::class); 60 | 61 | Route::post('forms/{form_name}/validate', [FormController::class, 'validateAgainstCustomInput'])->name('forms.validate'); 62 | Route::apiResource('forms', FormController::class); 63 | Route::apiResource('form-templates', FormTemplateController::class); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/App/Contracts/CustomField.php: -------------------------------------------------------------------------------- 1 | data = $data; 15 | parent::__construct($message, $code, $previous); 16 | } 17 | 18 | public function getData(): array 19 | { 20 | return $this->data; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/App/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | , \Psr\Log\LogLevel::*> 13 | */ 14 | protected $levels = [ 15 | // 16 | ]; 17 | 18 | /** 19 | * A list of the exception types that are not reported. 20 | * 21 | * @var array> 22 | */ 23 | protected $dontReport = [ 24 | // 25 | ]; 26 | 27 | /** 28 | * A list of the inputs that are never flashed to the session on validation exceptions. 29 | * 30 | * @var array 31 | */ 32 | protected $dontFlash = [ 33 | 'current_password', 34 | 'password', 35 | 'password_confirmation', 36 | ]; 37 | 38 | /** 39 | * Register the exception handling callbacks for the application. 40 | * 41 | * @return void 42 | */ 43 | public function register() 44 | { 45 | $this->renderable(function (MissingRequiredFieldException $e) { 46 | return response()->json([ 47 | 'message' => $e->getMessage(), 48 | 'errors' => $e->getData(), 49 | ], $e->getCode()); 50 | }); 51 | 52 | $this->renderable(function (FieldValidationException $e) { 53 | return response()->json([ 54 | 'message' => $e->getMessage(), 55 | 'errors' => $e->getData(), 56 | ], $e->getCode()); 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/App/Exceptions/MissingRequiredFieldException.php: -------------------------------------------------------------------------------- 1 | data = $data; 15 | parent::__construct($message, $code, $previous); 16 | } 17 | 18 | public function getData(): array 19 | { 20 | return $this->data; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | customField = $customField; 21 | } 22 | 23 | /** 24 | * Display a listing of the resource. 25 | * 26 | * @return JsonResponse 27 | */ 28 | public function index(): JsonResponse 29 | { 30 | return response()->json($this->customField::all()); 31 | } 32 | 33 | /** 34 | * Store a newly created resource in storage. 35 | * 36 | * @param CustomFieldCreateRequest $request 37 | * @return JsonResponse 38 | */ 39 | public function store(CustomFieldCreateRequest $request): JsonResponse 40 | { 41 | $customField = $this->customField::query()->create($request->validated()); 42 | 43 | return response()->json($customField->refresh()); 44 | } 45 | 46 | /** 47 | * Display the specified resource. 48 | * 49 | * @param CustomField $customField 50 | * @return JsonResponse 51 | */ 52 | public function show(CustomField $customField): JsonResponse 53 | { 54 | return response()->json($customField); 55 | } 56 | 57 | /** 58 | * Update the specified resource in storage. 59 | * 60 | * @param CustomFieldUpdateRequest $request 61 | * @param CustomField $customField 62 | * @return JsonResponse 63 | */ 64 | public function update(CustomFieldUpdateRequest $request, CustomField $customField): JsonResponse 65 | { 66 | $customField->update($request->validated()); 67 | 68 | return response()->json($customField->refresh()); 69 | } 70 | 71 | /** 72 | * Remove the specified resource from storage. 73 | * 74 | * @param CustomField $customField 75 | * @return JsonResponse 76 | * 77 | * @throws Exception 78 | */ 79 | public function destroy(CustomField $customField): JsonResponse 80 | { 81 | $isDeleted = $customField->delete(); 82 | 83 | return response()->json($isDeleted ? 'true' : 'false'); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/FormController.php: -------------------------------------------------------------------------------- 1 | form = $form; 24 | } 25 | 26 | /** 27 | * Display a listing of the resource. 28 | * 29 | * @return JsonResponse 30 | */ 31 | public function index(): JsonResponse 32 | { 33 | return response()->json($this->form::all()); 34 | } 35 | 36 | /** 37 | * Store a newly created resource in storage. 38 | * 39 | * @param FormRequest $request 40 | * @return JsonResponse 41 | */ 42 | public function store(FormRequest $request): JsonResponse 43 | { 44 | $form = $this->form::query()->create($request->validated()); 45 | 46 | return response()->json($form->refresh()); 47 | } 48 | 49 | /** 50 | * Display the specified resource. 51 | * 52 | * @param Form $form 53 | * @return JsonResponse 54 | */ 55 | public function show(Form $form): JsonResponse 56 | { 57 | return response()->json($form); 58 | } 59 | 60 | /** 61 | * Update the specified resource in storage. 62 | * 63 | * @param FormRequest $request 64 | * @param Form $form 65 | * @return JsonResponse 66 | */ 67 | public function update(FormRequest $request, Form $form): JsonResponse 68 | { 69 | $form->update($request->validated()); 70 | 71 | return response()->json($form->refresh()); 72 | } 73 | 74 | /** 75 | * Remove the specified resource from storage. 76 | * 77 | * @param Form $form 78 | * @return JsonResponse 79 | * 80 | * @throws Exception 81 | */ 82 | public function destroy(Form $form): JsonResponse 83 | { 84 | $isDeleted = $form->delete(); 85 | 86 | return response()->json($isDeleted ? 'true' : 'false'); 87 | } 88 | 89 | /** 90 | * @param Request $request 91 | * @param $formName 92 | * @return JsonResponse 93 | * 94 | * @throws Exception 95 | */ 96 | public function validateAgainstCustomInput(Request $request, $formName) 97 | { 98 | /** 99 | * @var Form $form 100 | */ 101 | $form = $this->form::query()->where('name', $formName)->firstOrFail(); 102 | 103 | return response()->json($form->validate($request->all())); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/FormTemplateController.php: -------------------------------------------------------------------------------- 1 | formTemplate = $formTemplate; 18 | } 19 | 20 | /** 21 | * Display a listing of the resource. 22 | * 23 | * @return JsonResponse 24 | */ 25 | public function index(): JsonResponse 26 | { 27 | return response()->json($this->formTemplate::all()); 28 | } 29 | 30 | /** 31 | * Store a newly created resource in storage. 32 | * 33 | * @param FormTemplateRequest $request 34 | * @return JsonResponse 35 | */ 36 | public function store(FormTemplateRequest $request): JsonResponse 37 | { 38 | $formTemplate = $this->formTemplate::query() 39 | ->create(Arr::except($request->validated(), 'form_data')); 40 | 41 | $formTemplate->createCustomFieldValues(Arr::get($request->validated(), 'form_data', [])); 42 | 43 | return response()->json($formTemplate->refresh()->load('customFieldValues.customField.selectable'), JsonResponse::HTTP_CREATED); 44 | } 45 | 46 | /** 47 | * Display the specified resource. 48 | * 49 | * @param FormTemplate $formTemplate 50 | * @return JsonResponse 51 | */ 52 | public function show(FormTemplate $formTemplate): JsonResponse 53 | { 54 | return response()->json($formTemplate); 55 | } 56 | 57 | /** 58 | * Update the specified resource in storage. 59 | * 60 | * @param FormTemplateRequest $request 61 | * @param FormTemplate $formTemplate 62 | * @return JsonResponse 63 | */ 64 | public function update(FormTemplateRequest $request, FormTemplate $formTemplate): JsonResponse 65 | { 66 | $formTemplate->createCustomFieldValues(Arr::get($request->validated(), 'form_data', [])); 67 | $formTemplate->update(Arr::except($request->validated(), 'form_data')); 68 | 69 | return response()->json($formTemplate->refresh()->load('customFieldValues.customField.selectable')); 70 | } 71 | 72 | /** 73 | * Remove the specified resource from storage. 74 | * 75 | * @param FormTemplate $formTemplate 76 | * @return JsonResponse 77 | */ 78 | public function destroy(FormTemplate $formTemplate): JsonResponse 79 | { 80 | $isDeleted = $formTemplate->delete(); 81 | 82 | return response()->json($isDeleted ? 'true' : 'false'); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/ModelController.php: -------------------------------------------------------------------------------- 1 | json($this->getModelsWithTrait($traitPath)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/PlainCustomFieldController.php: -------------------------------------------------------------------------------- 1 | customField = $customField; 23 | $this->mappings = config('asseco-custom-fields.plain_types'); 24 | } 25 | 26 | /** 27 | * Display a listing of the resource. 28 | * 29 | * @path plain_type string One of the plain types (string, text, integer, float, date, boolean) 30 | * 31 | * @multiple true 32 | * 33 | * @param string|null $type 34 | * @return JsonResponse 35 | */ 36 | public function index(?string $type = null): JsonResponse 37 | { 38 | return response()->json($this->customField::plain($type)->get()); 39 | } 40 | 41 | /** 42 | * Store a newly created resource in storage. 43 | * 44 | * @path plain_type string One of the plain types (string, text, integer, float, date, boolean) 45 | * 46 | * @except selectable_type selectable_id 47 | * 48 | * @param PlainCustomFieldRequest $request 49 | * @param string $type 50 | * @return JsonResponse 51 | */ 52 | public function store(PlainCustomFieldRequest $request, string $type): JsonResponse 53 | { 54 | $data = $request->validated(); 55 | 56 | /** @var Model $typeModel */ 57 | $typeModel = $this->mappings[$type]; 58 | 59 | $selectableData = [ 60 | 'selectable_type' => $typeModel, 61 | 'selectable_id' => $typeModel::query()->firstOrFail('id')->id, 62 | ]; 63 | 64 | $customField = $this->customField::query()->create(array_merge($data, $selectableData)); 65 | 66 | return response()->json($customField->refresh()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/RelationController.php: -------------------------------------------------------------------------------- 1 | relation = $relation; 23 | } 24 | 25 | /** 26 | * Display a listing of the resource. 27 | * 28 | * @return JsonResponse 29 | */ 30 | public function index(): JsonResponse 31 | { 32 | return response()->json($this->relation::all()); 33 | } 34 | 35 | /** 36 | * Store a newly created resource in storage. 37 | * 38 | * @param RelationRequest $request 39 | * @return JsonResponse 40 | */ 41 | public function store(RelationRequest $request): JsonResponse 42 | { 43 | $relation = $this->relation::query()->create($request->validated()); 44 | 45 | return response()->json($relation->refresh()); 46 | } 47 | 48 | /** 49 | * Display the specified resource. 50 | * 51 | * @param Relation $relation 52 | * @return JsonResponse 53 | */ 54 | public function show(Relation $relation): JsonResponse 55 | { 56 | return response()->json($relation); 57 | } 58 | 59 | /** 60 | * Update the specified resource in storage. 61 | * 62 | * @param RelationRequest $request 63 | * @param Relation $relation 64 | * @return JsonResponse 65 | */ 66 | public function update(RelationRequest $request, Relation $relation): JsonResponse 67 | { 68 | $relation->update($request->validated()); 69 | 70 | return response()->json($relation->refresh()); 71 | } 72 | 73 | /** 74 | * Remove the specified resource from storage. 75 | * 76 | * @param Relation $relation 77 | * @return JsonResponse 78 | * 79 | * @throws Exception 80 | */ 81 | public function destroy(Relation $relation): JsonResponse 82 | { 83 | $isDeleted = $relation->delete(); 84 | 85 | return response()->json($isDeleted ? 'true' : 'false'); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/RemoteCustomFieldController.php: -------------------------------------------------------------------------------- 1 | customField = $customField; 33 | $this->remoteClass = $remoteType; 34 | $this->plainType = $plainType; 35 | } 36 | 37 | /** 38 | * Display a listing of the resource. 39 | * 40 | * @return JsonResponse 41 | */ 42 | public function index(): JsonResponse 43 | { 44 | return response()->json($this->customField::remote()->with('selectable')->get()); 45 | } 46 | 47 | /** 48 | * Store a newly created resource in storage. 49 | * 50 | * @except selectable_type selectable_id 51 | * 52 | * @append remote RemoteType 53 | * 54 | * @param RemoteCustomFieldRequest $request 55 | * @return JsonResponse 56 | */ 57 | public function store(RemoteCustomFieldRequest $request): JsonResponse 58 | { 59 | $data = $request->validated(); 60 | 61 | /** @var CustomField $customField */ 62 | $customField = DB::transaction(function () use ($data) { 63 | // Force casting remote types to string unless we decide on different implementation. 64 | $plainTypeId = $this->plainType::query()->where('name', 'string')->firstOrFail()->id; 65 | 66 | $remoteType = $this->remoteClass::query()->create(array_merge( 67 | Arr::get($data, 'remote'), ['plain_type_id' => $plainTypeId])); 68 | 69 | $selectableData = [ 70 | 'selectable_type' => get_class($this->remoteClass), 71 | 'selectable_id' => $remoteType->id, 72 | ]; 73 | 74 | $cfData = Arr::except($data, 'remote'); 75 | 76 | return $this->customField::query()->create( 77 | array_merge($cfData, $selectableData, ['plain_type_id' => $plainTypeId]) 78 | ); 79 | }); 80 | 81 | return response()->json($customField->refresh()->load('selectable')); 82 | } 83 | 84 | /** 85 | * Update the specified resource in storage. 86 | * 87 | * @param RemoteTypeRequest $request 88 | * @param RemoteType $remoteType 89 | * @return JsonResponse 90 | */ 91 | public function update(RemoteTypeRequest $request, RemoteType $remoteType): JsonResponse 92 | { 93 | $remoteType->update($request->validated()); 94 | 95 | return response()->json($remoteType->refresh()); 96 | } 97 | 98 | /** 99 | * Display the specified resource. 100 | * 101 | * @param RemoteType $remoteType 102 | * @return JsonResponse 103 | */ 104 | public function resolve(RemoteType $remoteType): JsonResponse 105 | { 106 | $data = $remoteType->getRemoteData(); 107 | 108 | $data = $remoteType->data_path ? Arr::get($data, $remoteType->data_path) : $data; 109 | $transformed = $this->transform($data, $remoteType->mappings); 110 | 111 | return response()->json($transformed); 112 | } 113 | 114 | /** 115 | * Display the specified resource. 116 | * 117 | * @param RemoteType $remoteType 118 | * @param string $identifierValue 119 | * @return JsonResponse 120 | */ 121 | public function resolveByIdentifierValue(RemoteType $remoteType, string $identifierValue): JsonResponse 122 | { 123 | $data = $remoteType->getRemoteData(); 124 | 125 | $data = $remoteType->data_path ? Arr::get($data, $remoteType->data_path) : $data; 126 | 127 | $data = collect($data)->where($remoteType->identifier_property, $identifierValue)->first(); 128 | 129 | $transformed = is_array($data) ? $this->mapSingle($remoteType->mappings, $data) : $data; 130 | 131 | return response()->json($transformed); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/SelectionCustomFieldController.php: -------------------------------------------------------------------------------- 1 | customField = $customField; 29 | $this->selectionClass = config('asseco-custom-fields.models.selection_type'); 30 | } 31 | 32 | /** 33 | * Display a listing of the resource. 34 | * 35 | * @multiple true 36 | * 37 | * @return JsonResponse 38 | */ 39 | public function index(): JsonResponse 40 | { 41 | return response()->json($this->customField::selection()->with('selectable')->get()); 42 | } 43 | 44 | /** 45 | * Store a newly created resource in storage. 46 | * 47 | * @path plain_type string One of the plain types (string, text, integer, float, date, boolean) 48 | * 49 | * @except selectable_type selectable_id 50 | * 51 | * @append selection SelectionType 52 | * @append values SelectionValue 53 | * 54 | * @param SelectionCustomFieldRequest $request 55 | * @param string $type 56 | * @return JsonResponse 57 | */ 58 | public function store(SelectionCustomFieldRequest $request, string $type): JsonResponse 59 | { 60 | $data = $request->validated(); 61 | 62 | /** @var CustomField $customField */ 63 | $customField = DB::transaction(function () use ($data, $type) { 64 | $selectionData = Arr::get($data, 'selection', []); 65 | $multiselect = Arr::get($selectionData, 'multiselect', false); 66 | 67 | /** @var PlainType $plainType */ 68 | $plainType = app(PlainType::class); 69 | $plainTypeId = $plainType::query()->where('name', $type)->firstOrFail()->id; 70 | 71 | /** 72 | * @var Model $selectionTypeModel 73 | */ 74 | $selectionTypeModel = $this->selectionClass; 75 | $selectionType = $selectionTypeModel::query()->create([ 76 | 'plain_type_id' => $plainTypeId, 77 | 'multiselect' => $multiselect, 78 | ]); 79 | 80 | $selectionValues = Arr::get($data, 'values', []); 81 | 82 | foreach ($selectionValues as $value) { 83 | /** @var SelectionValue $selectionValue */ 84 | $selectionValue = app(SelectionValue::class); 85 | $selectionValue::query()->create( 86 | array_merge($value, ['selection_type_id' => $selectionType->id]) 87 | ); 88 | } 89 | 90 | $selectableData = [ 91 | 'selectable_type' => $this->selectionClass, 92 | 'selectable_id' => $selectionType->id, 93 | ]; 94 | 95 | $cfData = Arr::except($data, ['selection', 'values']); 96 | 97 | return $this->customField::query()->create(array_merge($cfData, $selectableData)); 98 | }); 99 | 100 | return response()->json($customField->refresh()->load('selectable.values')); 101 | } 102 | 103 | /** 104 | * Update the specified resource in storage. 105 | * 106 | * @param SelectionTypeRequest $request 107 | * @param SelectionType $selectionType 108 | * @return JsonResponse 109 | */ 110 | public function update(SelectionTypeRequest $request, SelectionType $selectionType): JsonResponse 111 | { 112 | $selectionType->update($request->validated()); 113 | 114 | return response()->json($selectionType->refresh()); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/SelectionValueController.php: -------------------------------------------------------------------------------- 1 | selectionValue = $selectionValue; 23 | } 24 | 25 | /** 26 | * Display a listing of the resource. 27 | * 28 | * @return JsonResponse 29 | */ 30 | public function index(): JsonResponse 31 | { 32 | return response()->json($this->selectionValue::all()); 33 | } 34 | 35 | /** 36 | * Store a newly created resource in storage. 37 | * 38 | * @param SelectionValueRequest $request 39 | * @return JsonResponse 40 | */ 41 | public function store(SelectionValueRequest $request): JsonResponse 42 | { 43 | if (method_exists($this->selectionValue, 'bootSoftDeletes')) { 44 | // check for deleted values 45 | $selectionValue = $this->selectionValue::withTrashed() 46 | ->where('selection_type_id', $request->get('selection_type_id')) 47 | ->where('value', $request->get('value')) 48 | ->first(); 49 | 50 | if ($selectionValue) { 51 | if ($selectionValue->trashed()) { 52 | // restore 53 | $selectionValue->restoreQuietly(); 54 | $selectionValue->update($request->validated()); 55 | } else { 56 | throw new Exception('Selection value already exists.', 400); 57 | } 58 | } else { 59 | $selectionValue = $this->selectionValue::query()->create($request->validated()); 60 | } 61 | } else { 62 | $selectionValue = $this->selectionValue::query()->create($request->validated()); 63 | } 64 | 65 | return response()->json($selectionValue->refresh()); 66 | } 67 | 68 | /** 69 | * Display the specified resource. 70 | * 71 | * @param SelectionValue $selectionValue 72 | * @return JsonResponse 73 | */ 74 | public function show(SelectionValue $selectionValue): JsonResponse 75 | { 76 | return response()->json($selectionValue); 77 | } 78 | 79 | /** 80 | * Update the specified resource in storage. 81 | * 82 | * @param SelectionValueRequest $request 83 | * @param SelectionValue $selectionValue 84 | * @return JsonResponse 85 | */ 86 | public function update(SelectionValueRequest $request, SelectionValue $selectionValue): JsonResponse 87 | { 88 | $selectionValue->update($request->validated()); 89 | 90 | return response()->json($selectionValue->refresh()); 91 | } 92 | 93 | /** 94 | * Remove the specified resource from storage. 95 | * 96 | * @param SelectionValue $selectionValue 97 | * @return JsonResponse 98 | * 99 | * @throws Exception 100 | */ 101 | public function destroy(SelectionValue $selectionValue): JsonResponse 102 | { 103 | $isDeleted = $selectionValue->delete(); 104 | 105 | return response()->json($isDeleted ? 'true' : 'false'); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/TypeController.php: -------------------------------------------------------------------------------- 1 | json($customField::types()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/ValidationController.php: -------------------------------------------------------------------------------- 1 | validation = $validation; 23 | } 24 | 25 | /** 26 | * Display a listing of the resource. 27 | * 28 | * @return JsonResponse 29 | */ 30 | public function index(): JsonResponse 31 | { 32 | return response()->json($this->validation::all()); 33 | } 34 | 35 | /** 36 | * Store a newly created resource in storage. 37 | * 38 | * @param ValidationRequest $request 39 | * @return JsonResponse 40 | */ 41 | public function store(ValidationRequest $request): JsonResponse 42 | { 43 | $customFieldValidation = $this->validation::query()->create($request->validated()); 44 | 45 | return response()->json($customFieldValidation->refresh()); 46 | } 47 | 48 | /** 49 | * Display the specified resource. 50 | * 51 | * @param Validation $validation 52 | * @return JsonResponse 53 | */ 54 | public function show(Validation $validation): JsonResponse 55 | { 56 | return response()->json($validation); 57 | } 58 | 59 | /** 60 | * Update the specified resource in storage. 61 | * 62 | * @param ValidationRequest $request 63 | * @param Validation $validation 64 | * @return JsonResponse 65 | */ 66 | public function update(ValidationRequest $request, Validation $validation): JsonResponse 67 | { 68 | $validation->update($request->validated()); 69 | 70 | return response()->json($validation->refresh()); 71 | } 72 | 73 | /** 74 | * Remove the specified resource from storage. 75 | * 76 | * @param Validation $validation 77 | * @return JsonResponse 78 | * 79 | * @throws Exception 80 | */ 81 | public function destroy(Validation $validation): JsonResponse 82 | { 83 | $isDeleted = $validation->delete(); 84 | 85 | return response()->json($isDeleted ? 'true' : 'false'); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/App/Http/Controllers/ValueController.php: -------------------------------------------------------------------------------- 1 | value = $value; 24 | } 25 | 26 | /** 27 | * Display a listing of the resource. 28 | * 29 | * @return JsonResponse 30 | */ 31 | public function index(): JsonResponse 32 | { 33 | return response()->json($this->value::all()); 34 | } 35 | 36 | /** 37 | * Store a newly created resource in storage. 38 | * 39 | * @param ValueRequest $request 40 | * @return JsonResponse 41 | * 42 | * @throws Throwable 43 | */ 44 | public function store(ValueRequest $request): JsonResponse 45 | { 46 | $this->value::validateCreate($request); 47 | 48 | $value = $this->value::query()->create($request->validated()); 49 | 50 | return response()->json($value->refresh()); 51 | } 52 | 53 | /** 54 | * Display the specified resource. 55 | * 56 | * @param Value $value 57 | * @return JsonResponse 58 | */ 59 | public function show(Value $value): JsonResponse 60 | { 61 | return response()->json($value); 62 | } 63 | 64 | /** 65 | * Update the specified resource in storage. 66 | * 67 | * @param ValueRequest $request 68 | * @param Value $value 69 | * @return JsonResponse 70 | * 71 | * @throws Throwable 72 | */ 73 | public function update(ValueRequest $request, Value $value): JsonResponse 74 | { 75 | $value->validateUpdate($request); 76 | 77 | $value->update($request->validated()); 78 | 79 | return response()->json($value->refresh()); 80 | } 81 | 82 | /** 83 | * Remove the specified resource from storage. 84 | * 85 | * @param Value $value 86 | * @return JsonResponse 87 | * 88 | * @throws Exception 89 | */ 90 | public function destroy(Value $value): JsonResponse 91 | { 92 | $isDeleted = $value->delete(); 93 | 94 | return response()->json($isDeleted ? 'true' : 'false'); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/App/Http/Requests/CustomFieldCreateRequest.php: -------------------------------------------------------------------------------- 1 | [ 33 | 'required', 34 | 'string', 35 | 'regex:/^[^\s]*$/i', 36 | Rule::unique('custom_fields')->ignore($this->custom_field)->where(function ($query) { 37 | return $this->usesSoftDelete() ? $query->whereNull('deleted_at') : $query; 38 | }), 39 | ], 40 | 'label' => 'required|string|max:255', 41 | 'placeholder' => 'nullable|string', 42 | 'selectable_type' => 'required', 43 | 'selectable_id' => 'required', 44 | 'model' => 'required|string', 45 | 'required' => 'boolean', 46 | 'hidden' => 'boolean', 47 | 'validation_id' => 'nullable|exists:custom_field_validations,id', 48 | 'group' => 'nullable|string', 49 | 'order' => 'nullable|integer', 50 | 'renderer' => 'nullable|string', 51 | ]; 52 | } 53 | 54 | /** 55 | * Get the error messages for the defined validation rules. 56 | * 57 | * @return array 58 | */ 59 | public function messages() 60 | { 61 | return [ 62 | 'name.regex' => 'Custom field name must not contain spaces.', 63 | ]; 64 | } 65 | 66 | private function usesSoftDelete(): bool 67 | { 68 | return in_array(SoftDeletes::class, class_uses_recursive(app(CustomFieldContract::class))); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/App/Http/Requests/CustomFieldUpdateRequest.php: -------------------------------------------------------------------------------- 1 | [ 39 | 'required', 40 | 'string', 41 | 'regex:/^[^\s]*$/i', 42 | Rule::unique('custom_fields')->ignore($this->custom_field)->where(function ($query) { 43 | return $this->usesSoftDelete() ? $query->whereNull('deleted_at') : $query; 44 | }), 45 | ], 46 | 'label' => 'sometimes|string|max:255', 47 | 'placeholder' => 'nullable|string', 48 | 'required' => 'boolean', 49 | 'hidden' => 'boolean', 50 | 'validation_id' => 'nullable|exists:custom_field_validations,id', 51 | 'group' => 'nullable|string', 52 | 'order' => 'nullable|integer', 53 | 'renderer' => 'nullable|string', 54 | ]; 55 | 56 | return Arr::except($rules, self::LOCKED_FOR_EDITING); 57 | } 58 | 59 | /** 60 | * Get the error messages for the defined validation rules. 61 | * 62 | * @return array 63 | */ 64 | public function messages() 65 | { 66 | return [ 67 | 'name.regex' => 'Custom field name must not contain spaces.', 68 | ]; 69 | } 70 | 71 | private function usesSoftDelete(): bool 72 | { 73 | return in_array(SoftDeletes::class, class_uses_recursive(app(CustomFieldContract::class))); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/App/Http/Requests/FormRequest.php: -------------------------------------------------------------------------------- 1 | 'nullable', 31 | 'name' => 'required|string|regex:/^[^\s]*$/i|unique:forms,name' . ($this->form ? ',' . $this->form->id : null), 32 | 'definition' => 'required|array', 33 | 'action_url' => 'nullable|string', 34 | ]; 35 | } 36 | 37 | /** 38 | * Get the error messages for the defined validation rules. 39 | * 40 | * @return array 41 | */ 42 | public function messages() 43 | { 44 | return [ 45 | 'name.regex' => 'Form name must not contain spaces.', 46 | ]; 47 | } 48 | 49 | /** 50 | * Dynamically set validator from 'required' to 'sometimes' if resource is being updated. 51 | * 52 | * @param Validator $validator 53 | */ 54 | public function withValidator(Validator $validator) 55 | { 56 | $requiredOnCreate = ['name', 'definition']; 57 | 58 | $validator->sometimes($requiredOnCreate, 'sometimes', function () { 59 | return $this->form !== null; 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/App/Http/Requests/FormTemplateRequest.php: -------------------------------------------------------------------------------- 1 | 'array', 13 | 'form_id' => 'required|string|exists:forms,id', 14 | 'name' => 'required|string', 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/App/Http/Requests/PlainCustomFieldRequest.php: -------------------------------------------------------------------------------- 1 | [ 33 | 'required', 34 | 'string', 35 | 'regex:/^[^\s]*$/i', 36 | Rule::unique('custom_fields')->ignore($this->custom_field)->where(function ($query) { 37 | return $this->usesSoftDelete() ? $query->whereNull('deleted_at') : $query; 38 | }), 39 | ], 40 | 'label' => 'required|string|max:255', 41 | 'placeholder' => 'nullable|string', 42 | 'model' => 'required|string', 43 | 'required' => 'boolean', 44 | 'validation_id' => 'nullable|exists:custom_field_validations', 45 | 'group' => 'nullable|string', 46 | 'order' => 'nullable|integer', 47 | 'renderer' => 'nullable|string', 48 | ]; 49 | } 50 | 51 | /** 52 | * Get the error messages for the defined validation rules. 53 | * 54 | * @return array 55 | */ 56 | public function messages() 57 | { 58 | return [ 59 | 'name.regex' => 'Custom field name must not contain spaces.', 60 | ]; 61 | } 62 | 63 | private function usesSoftDelete(): bool 64 | { 65 | return in_array(SoftDeletes::class, class_uses_recursive(app(CustomFieldContract::class))); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/App/Http/Requests/RelationRequest.php: -------------------------------------------------------------------------------- 1 | 'required|exists:custom_fields,id', 31 | 'child_id' => 'required|exists:custom_fields,id|different:parent_id', 32 | ]; 33 | } 34 | 35 | /** 36 | * Dynamically set validator from 'required' to 'sometimes' if resource is being updated. 37 | * 38 | * @param Validator $validator 39 | */ 40 | public function withValidator(Validator $validator) 41 | { 42 | $requiredOnCreate = ['parent_id', 'child_id']; 43 | 44 | $validator->sometimes($requiredOnCreate, 'sometimes', function () { 45 | return $this->relation !== null; 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/App/Http/Requests/RemoteCustomFieldRequest.php: -------------------------------------------------------------------------------- 1 | [ 33 | 'required', 34 | 'string', 35 | 'regex:/^[^\s]*$/i', 36 | Rule::unique('custom_fields')->ignore($this->custom_field)->where(function ($query) { 37 | return $this->usesSoftDelete() ? $query->whereNull('deleted_at') : $query; 38 | }), 39 | ], 40 | 'label' => 'required|string|max:255', 41 | 'placeholder' => 'nullable|string', 42 | 'model' => 'required|string', 43 | 'required' => 'boolean', 44 | 'validation_id' => 'nullable|exists:custom_field_validations', 45 | 'group' => 'nullable|string', 46 | 'order' => 'nullable|integer', 47 | 'renderer' => 'nullable|string', 48 | 'remote' => 'required|array', 49 | 'remote.url' => 'required|url', 50 | 'remote.method' => 'required|in:GET,POST,PUT', 51 | 'remote.body' => 'nullable|array', 52 | 'remote.headers' => 'nullable|array', 53 | 'remote.mappings' => 'nullable|array', 54 | 'remote.data_path' => 'nullable|string', 55 | 'remote.identifier_property' => 'nullable|string', 56 | ]; 57 | } 58 | 59 | /** 60 | * Get the error messages for the defined validation rules. 61 | * 62 | * @return array 63 | */ 64 | public function messages() 65 | { 66 | return [ 67 | 'name.regex' => 'Custom field name must not contain spaces.', 68 | ]; 69 | } 70 | 71 | private function usesSoftDelete(): bool 72 | { 73 | return in_array(SoftDeletes::class, class_uses_recursive(app(CustomFieldContract::class))); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/App/Http/Requests/RemoteTypeRequest.php: -------------------------------------------------------------------------------- 1 | 'url', 30 | 'method' => 'in:GET,POST,PUT', 31 | 'body' => 'nullable|array', 32 | 'headers' => 'nullable|array', 33 | 'mappings' => 'nullable|array', 34 | 'data_path' => 'nullable|string', 35 | 'identifier_property' => 'nullable|string', 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App/Http/Requests/SelectionCustomFieldRequest.php: -------------------------------------------------------------------------------- 1 | [ 33 | 'required', 34 | 'string', 35 | 'regex:/^[^\s]*$/i', 36 | Rule::unique('custom_fields')->ignore($this->custom_field)->where(function ($query) { 37 | return $this->usesSoftDelete() ? $query->whereNull('deleted_at') : $query; 38 | }), 39 | ], 40 | 'label' => 'required|string|max:255', 41 | 'placeholder' => 'nullable|string', 42 | 'model' => 'required|string', 43 | 'required' => 'boolean', 44 | 'validation_id' => 'nullable|exists:custom_field_validations', 45 | 'group' => 'nullable|string', 46 | 'order' => 'nullable|integer', 47 | 'renderer' => 'nullable|string', 48 | 'selection' => 'array', 49 | 'selection.multiselect' => 'boolean', 50 | 'values' => 'array', 51 | 'values.*.label' => 'nullable|string', 52 | 'values.*.value' => 'string|required_with:values', 53 | 'values.*.preselect' => 'boolean', 54 | ]; 55 | } 56 | 57 | /** 58 | * Get the error messages for the defined validation rules. 59 | * 60 | * @return array 61 | */ 62 | public function messages() 63 | { 64 | return [ 65 | 'name.regex' => 'Custom field name must not contain spaces.', 66 | ]; 67 | } 68 | 69 | private function usesSoftDelete(): bool 70 | { 71 | return in_array(SoftDeletes::class, class_uses_recursive(app(CustomFieldContract::class))); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/App/Http/Requests/SelectionTypeRequest.php: -------------------------------------------------------------------------------- 1 | 'boolean', 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/App/Http/Requests/SelectionValueRequest.php: -------------------------------------------------------------------------------- 1 | 'required|exists:custom_field_selection_types,id', 31 | 'label' => 'nullable|string', 32 | 'value' => 'required|string', 33 | 'preselect' => 'boolean', 34 | ]; 35 | } 36 | 37 | /** 38 | * Dynamically set validator from 'required' to 'sometimes' if resource is being updated. 39 | * 40 | * @param Validator $validator 41 | */ 42 | public function withValidator(Validator $validator) 43 | { 44 | $requiredOnCreate = ['selection_type_id', 'value']; 45 | 46 | $validator->sometimes($requiredOnCreate, 'sometimes', function () { 47 | return $this->selection_value !== null; 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/App/Http/Requests/ValidationRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string|max:150|unique:custom_field_validations,name' . ($this->validation ? ',' . $this->validation->id : null), 31 | 'regex' => 'nullable|string|max:255', 32 | 'generic' => 'boolean', 33 | ]; 34 | } 35 | 36 | /** 37 | * Dynamically set validator from 'required' to 'sometimes' if resource is being updated. 38 | * 39 | * @param Validator $validator 40 | */ 41 | public function withValidator(Validator $validator) 42 | { 43 | $requiredOnCreate = ['name']; 44 | 45 | $validator->sometimes($requiredOnCreate, 'sometimes', function () { 46 | return $this->validation !== null; 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/App/Http/Requests/ValueRequest.php: -------------------------------------------------------------------------------- 1 | 'required|exists:custom_fields,id', 31 | 'model_type' => 'required|string', 32 | 'model_id' => 'required', 33 | 'string' => 'nullable|string|max:255', 34 | 'integer' => 'nullable|integer', 35 | 'float' => 'nullable|numeric', 36 | 'text' => 'nullable|string', 37 | 'boolean' => 'nullable|boolean', 38 | 'datetime' => 'nullable|string', 39 | 'date' => 'nullable|string', 40 | 'time' => 'nullable|string', 41 | 'json' => 'nullable|array', 42 | ]; 43 | } 44 | 45 | /** 46 | * Dynamically set validator from 'required' to 'sometimes' if resource is being updated. 47 | * 48 | * @param Validator $validator 49 | */ 50 | public function withValidator(Validator $validator) 51 | { 52 | $requiredOnCreate = ['custom_field_id', 'model_type', 'model_id']; 53 | 54 | $validator->sometimes($requiredOnCreate, 'sometimes', function () { 55 | return $this->value !== null; 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/App/Models/FormTemplate.php: -------------------------------------------------------------------------------- 1 | belongsTo(get_class(app(Form::class))); 20 | } 21 | 22 | /** 23 | * @param array $formData 24 | * @return array 25 | */ 26 | public function createCustomFieldValues(array $formData = []): array 27 | { 28 | if (empty($formData)) { 29 | return []; 30 | } 31 | 32 | $values = []; 33 | 34 | /** 35 | * @var \Asseco\CustomFields\App\Contracts\CustomField $customField 36 | */ 37 | foreach ($this->form->customFields as $customField) { 38 | $formCustomField = Arr::get($formData, $customField->name); 39 | 40 | if (!$formCustomField) { 41 | continue; 42 | } 43 | 44 | $type = $customField->getValueColumn(); 45 | 46 | $values[] = $customField->values()->updateOrCreate([ 47 | 'model_type' => $this->getMorphClass(), 48 | 'model_id' => $this->id, 49 | ], 50 | [$type => $formCustomField] 51 | ); 52 | } 53 | 54 | return $values; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/App/Models/ParentType.php: -------------------------------------------------------------------------------- 1 | belongsTo(get_class(app(PlainType::class)), 'plain_type_id'); 17 | } 18 | 19 | public function subTypeClassPath(): string 20 | { 21 | $plainTypes = config('asseco-custom-fields.plain_types'); 22 | $typeName = $this->type->name; 23 | 24 | if (array_key_exists($typeName, $plainTypes)) { 25 | return $plainTypes[$typeName]; 26 | } 27 | 28 | return StringType::class; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/App/Models/PlainType.php: -------------------------------------------------------------------------------- 1 | 'array', 28 | 'headers' => 'array', 29 | 'mappings' => 'array', 30 | ]; 31 | 32 | protected static function newFactory() 33 | { 34 | return RemoteTypeFactory::new(); 35 | } 36 | 37 | public function customFields(): MorphMany 38 | { 39 | return $this->morphMany(get_class(app(CustomField::class)), 'selectable'); 40 | } 41 | 42 | public function getNameAttribute() 43 | { 44 | return 'remote'; 45 | } 46 | 47 | public function getRemoteData() 48 | { 49 | $cacheKey = 'remote_custom_field_' . $this->id; 50 | 51 | if (config('asseco-custom-fields.should_cache_remote') && Cache::has($cacheKey)) { 52 | return Cache::get($cacheKey); 53 | } 54 | 55 | $response = Http::withHeaders($this->getHeaders() ?: []) 56 | ->withBody($this->body, 'application/json') 57 | ->{$this->method}($this->url)->throw()->json(); 58 | 59 | Cache::put($cacheKey, $response, config('asseco-custom-fields.remote_cache_ttl')); 60 | 61 | return $response; 62 | } 63 | 64 | protected function getHeaders() 65 | { 66 | return $this->headers; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/App/Models/SelectionType.php: -------------------------------------------------------------------------------- 1 | morphMany(get_class(app(CustomField::class)), 'selectable'); 34 | } 35 | 36 | public function values(): HasMany 37 | { 38 | return $this->hasMany(get_class(app(SelectionValue::class))); 39 | } 40 | 41 | public function getNameAttribute() 42 | { 43 | return 'selection'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/App/Models/SelectionValue.php: -------------------------------------------------------------------------------- 1 | belongsTo(get_class(app(SelectionType::class))); 30 | } 31 | 32 | public function type(): HasOneThrough 33 | { 34 | return $this->hasOneThrough( 35 | get_class(app(PlainType::class)), 36 | get_class(app(SelectionType::class)), 37 | 'id', 38 | 'id', 39 | 'selection_type_id', 40 | 'plain_type_id' 41 | ); 42 | } 43 | 44 | /** 45 | * Accessor for casting value to appropriate type based on the actual plain type. 46 | * 47 | * @param $value 48 | * @return bool|float|int 49 | */ 50 | public function getValueAttribute($value) 51 | { 52 | $plainType = optional($this->type)->name; 53 | 54 | switch ($plainType) { 55 | case 'integer': 56 | return (int) $value; 57 | case 'float': 58 | return (float) $value; 59 | case 'boolean': 60 | return (bool) $value; 61 | default: 62 | return $value; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/App/Models/Validation.php: -------------------------------------------------------------------------------- 1 | hasMany(get_class(app(CustomField::class))); 30 | } 31 | 32 | /** 33 | * @param $input 34 | * @return void 35 | * 36 | * @throws FieldValidationException|\Throwable 37 | */ 38 | public function validate($input): void 39 | { 40 | $pattern = trim($this->regex, '/'); 41 | 42 | throw_if(!preg_match("/$pattern/", "$input"), new FieldValidationException("Provided data doesn't pass $pattern validation")); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/App/Models/Value.php: -------------------------------------------------------------------------------- 1 | 'float', 41 | 'boolean' => 'boolean', 42 | 'json' => 'array', 43 | ]; 44 | 45 | protected static function booted() 46 | { 47 | static::creating(function (self $customFieldValue) { 48 | $valueColumn = $customFieldValue->customField->getValueColumn(); 49 | switch ($valueColumn) { 50 | case 'date': 51 | $customFieldValue->{$valueColumn} = (new Carbon($customFieldValue->{$valueColumn}))->toDateString(); 52 | break; 53 | case 'time': 54 | $customFieldValue->{$valueColumn} = (new Carbon($customFieldValue->{$valueColumn}))->toTimeString(); 55 | break; 56 | case 'datetime': 57 | $customFieldValue->{$valueColumn} = (new Carbon($customFieldValue->{$valueColumn}))->format('Y-m-d H:i:s'); 58 | break; 59 | } 60 | }); 61 | } 62 | 63 | protected static function newFactory() 64 | { 65 | return ValueFactory::new(); 66 | } 67 | 68 | public function model(): MorphTo 69 | { 70 | return $this->morphTo(); 71 | } 72 | 73 | public function customField(): BelongsTo 74 | { 75 | return $this->belongsTo(get_class(app(CustomField::class))); 76 | } 77 | 78 | public function getValueAttribute() 79 | { 80 | foreach (self::VALUE_COLUMNS as $valueColumn) { 81 | if (isset($this->{$valueColumn})) { 82 | return $this->{$valueColumn}; 83 | } 84 | } 85 | 86 | return null; 87 | } 88 | 89 | /** 90 | * @param Request $request 91 | * 92 | * @throws Throwable 93 | */ 94 | public static function validateCreate(Request $request): void 95 | { 96 | /** @var CustomField $customFieldClass */ 97 | $customFieldClass = app(CustomField::class); 98 | /** 99 | * @var CustomField $customField 100 | */ 101 | $customField = $customFieldClass::query() 102 | ->with(['validation', 'selectable']) 103 | ->findOrFail($request->get('custom_field_id')); 104 | 105 | $valueColumn = $customField->getValueColumn(); 106 | 107 | self::filterByAllowedColumn($valueColumn, $request); 108 | 109 | throw_if(!$request->has($valueColumn) || is_null($request->get($valueColumn)), 110 | new Exception("Attribute '$valueColumn' needs to be provided.")); 111 | 112 | $customField->validate($request->get($valueColumn)); 113 | } 114 | 115 | /** 116 | * @param Request $request 117 | * 118 | * @throws Throwable 119 | */ 120 | public function validateUpdate(Request $request): void 121 | { 122 | /** 123 | * @var CustomField $customField 124 | */ 125 | $customField = $this->customField->load(['validation', 'selectable']); 126 | 127 | $mapToColumn = $customField->getValueColumn(); 128 | 129 | self::filterByAllowedColumn($mapToColumn, $request); 130 | 131 | if ($request->has($mapToColumn)) { 132 | $customField->validate($request->get($mapToColumn)); 133 | } 134 | } 135 | 136 | /** 137 | * @param string $valueColumn 138 | * @param Request $request 139 | * 140 | * @throws Throwable 141 | */ 142 | protected static function filterByAllowedColumn(string $valueColumn, Request $request): void 143 | { 144 | foreach (self::VALUE_COLUMNS as $column) { 145 | if ($column === $valueColumn) { 146 | continue; 147 | } 148 | 149 | $requestHasDisallowedColumn = $request->has($column) && $request->get($column); 150 | 151 | throw_if($requestHasDisallowedColumn, new Exception("Attribute '$column' is not allowed for this custom field, use '$valueColumn' instead.")); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/App/PlainTypes/BooleanType.php: -------------------------------------------------------------------------------- 1 | where('name', 'boolean'); 17 | }); 18 | } 19 | 20 | public static function mapToValueColumn(): string 21 | { 22 | return 'boolean'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/PlainTypes/DateTimeType.php: -------------------------------------------------------------------------------- 1 | where('name', 'datetime'); 17 | }); 18 | } 19 | 20 | public static function mapToValueColumn(): string 21 | { 22 | return 'datetime'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/PlainTypes/DateType.php: -------------------------------------------------------------------------------- 1 | where('name', 'date'); 17 | }); 18 | } 19 | 20 | public static function mapToValueColumn(): string 21 | { 22 | return 'date'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/PlainTypes/FloatType.php: -------------------------------------------------------------------------------- 1 | where('name', 'float'); 17 | }); 18 | } 19 | 20 | public static function mapToValueColumn(): string 21 | { 22 | return 'float'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/PlainTypes/IntegerType.php: -------------------------------------------------------------------------------- 1 | where('name', 'integer'); 17 | }); 18 | } 19 | 20 | public static function mapToValueColumn(): string 21 | { 22 | return 'integer'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/PlainTypes/JsonType.php: -------------------------------------------------------------------------------- 1 | where('name', 'json'); 17 | }); 18 | } 19 | 20 | public static function mapToValueColumn(): string 21 | { 22 | return 'json'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/PlainTypes/StringType.php: -------------------------------------------------------------------------------- 1 | where('name', 'string'); 17 | }); 18 | } 19 | 20 | public static function mapToValueColumn(): string 21 | { 22 | return 'string'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/PlainTypes/TextType.php: -------------------------------------------------------------------------------- 1 | where('name', 'text'); 17 | }); 18 | } 19 | 20 | public static function mapToValueColumn(): string 21 | { 22 | return 'text'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/PlainTypes/TimeType.php: -------------------------------------------------------------------------------- 1 | where('name', 'time'); 17 | }); 18 | } 19 | 20 | public static function mapToValueColumn(): string 21 | { 22 | return 'time'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/Traits/Customizable.php: -------------------------------------------------------------------------------- 1 | morphMany(get_class(app(Value::class)), 'model'); 15 | } 16 | 17 | /** 18 | * Append custom field key-value pairs to event. Key is CF name, value is exact value. 19 | * 20 | * @param array|null $customFieldValues 21 | * @return array 22 | */ 23 | public function flattenCustomFieldValues(?array $customFieldValues = null): array 24 | { 25 | $values = $customFieldValues ?: $this->customFieldValues->load('customField'); 26 | 27 | $mapped = []; 28 | 29 | foreach ($values as $value) { 30 | if (!$value instanceof Value) { 31 | continue; 32 | } 33 | 34 | $mapped[$value->customField->name] = $value->value; 35 | } 36 | 37 | return $mapped; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/App/Traits/FakesTypeValues.php: -------------------------------------------------------------------------------- 1 | unique()->randomNumber(); 14 | case 'float': 15 | return $faker->unique()->randomFloat(); 16 | case 'date': 17 | return $faker->unique()->date(); 18 | case 'time': 19 | return $faker->unique()->time(); 20 | case 'datetime': 21 | return $faker->unique()->datetime(); 22 | case 'text': 23 | return $faker->unique()->sentence; 24 | case 'boolean': 25 | return $faker->unique()->boolean; 26 | default: 27 | return $faker->unique()->word; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/App/Traits/FindsTraits.php: -------------------------------------------------------------------------------- 1 | $namespace) { 15 | $files = scandir($path); 16 | 17 | foreach ($files as $file) { 18 | if (stripos($file, '.php') === false) { 19 | continue; 20 | } 21 | 22 | $className = substr($file, 0, -4); 23 | $model = $namespace . $className; 24 | 25 | if (self::hasTrait($traitPath, $model)) { 26 | $models[] = $model; 27 | } 28 | } 29 | } 30 | 31 | return $models; 32 | } 33 | 34 | protected function hasTrait(string $traitPath, string $class): bool 35 | { 36 | $traits = class_uses($class); 37 | 38 | return in_array($traitPath, $traits, true); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/App/Traits/TransformsOutput.php: -------------------------------------------------------------------------------- 1 | mapSingle($mappings, $item); 18 | } 19 | 20 | return $transformed; 21 | } 22 | 23 | /** 24 | * @param array $mappings 25 | * @param array $item 26 | * @return array 27 | */ 28 | protected function mapSingle(array $mappings, array $item): array 29 | { 30 | $data = []; 31 | 32 | foreach ($mappings as $remoteKey => $localKey) { 33 | if (!array_key_exists($remoteKey, $item)) { 34 | continue; 35 | } 36 | 37 | $data = array_merge_recursive($data, [ 38 | $localKey => $item[$remoteKey], 39 | ]); 40 | } 41 | 42 | return $data; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/CustomFieldsServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . '/../config/asseco-custom-fields.php', 'asseco-custom-fields'); 38 | } 39 | 40 | /** 41 | * Bootstrap the application services. 42 | */ 43 | public function boot(): void 44 | { 45 | $this->bindClasses(); 46 | 47 | $this->loadRoutesFrom(__DIR__ . '/../routes/api.php'); 48 | 49 | if (config('asseco-custom-fields.migrations.run')) { 50 | $this->loadMigrationsFrom(__DIR__ . '/../migrations'); 51 | } 52 | 53 | $this->publishes([ 54 | __DIR__ . '/../migrations' => database_path('migrations'), 55 | ], 'asseco-custom-fields'); 56 | 57 | $this->publishes([ 58 | __DIR__ . '/../config/asseco-custom-fields.php' => config_path('asseco-custom-fields.php'), 59 | ], 'asseco-custom-fields'); 60 | 61 | $this->routeModelBinding(); 62 | } 63 | 64 | protected function bindClasses() 65 | { 66 | $this->app->bind(CustomField::class, config('asseco-custom-fields.models.custom_field')); 67 | $this->app->bind(Form::class, config('asseco-custom-fields.models.form')); 68 | $this->app->bind(PlainType::class, config('asseco-custom-fields.models.plain_type')); 69 | $this->app->bind(RemoteType::class, config('asseco-custom-fields.models.remote_type')); 70 | $this->app->bind(SelectionType::class, config('asseco-custom-fields.models.selection_type')); 71 | $this->app->bind(SelectionValue::class, config('asseco-custom-fields.models.selection_value')); 72 | $this->app->bind(Relation::class, config('asseco-custom-fields.models.relation')); 73 | $this->app->bind(Validation::class, config('asseco-custom-fields.models.validation')); 74 | $this->app->bind(Value::class, config('asseco-custom-fields.models.value')); 75 | $this->app->bind(FormTemplate::class, config('asseco-custom-fields.models.form_template')); 76 | 77 | $this->app->bind(BooleanType::class, config('asseco-custom-fields.plain_types.boolean')); 78 | $this->app->bind(DateTimeType::class, config('asseco-custom-fields.plain_types.date_time')); 79 | $this->app->bind(DateType::class, config('asseco-custom-fields.plain_types.date')); 80 | $this->app->bind(FloatType::class, config('asseco-custom-fields.plain_types.float')); 81 | $this->app->bind(IntegerType::class, config('asseco-custom-fields.plain_types.integer')); 82 | $this->app->bind(StringType::class, config('asseco-custom-fields.plain_types.string')); 83 | $this->app->bind(TextType::class, config('asseco-custom-fields.plain_types.text')); 84 | $this->app->bind(TimeType::class, config('asseco-custom-fields.plain_types.time')); 85 | 86 | $this->app->extend(ExceptionHandler::class, function (ExceptionHandler $handler, $app) { 87 | return new Handler($this->app); 88 | }); 89 | } 90 | 91 | protected function routeModelBinding() 92 | { 93 | Route::model('custom_field', get_class(app(CustomField::class))); 94 | Route::model('form', get_class(app(Form::class))); 95 | Route::model('form_template', get_class(app(FormTemplate::class))); 96 | Route::model('remote_type', get_class(app(RemoteType::class))); 97 | // Plain type pattern is defined in routes, so no need to register it here 98 | // Route::model('plain_type', get_class(app(PlainType::class))); 99 | Route::model('selection_type', get_class(app(SelectionType::class))); 100 | Route::model('selection_value', get_class(app(SelectionValue::class))); 101 | Route::model('relation', get_class(app(Relation::class))); 102 | Route::model('validation', get_class(app(Validation::class))); 103 | Route::model('value', get_class(app(Value::class))); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Database/Factories/CustomFieldFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->word, 25 | 'selectable_id' => $this->faker->randomNumber(), 26 | 'name' => implode('_', $this->faker->words(5)), 27 | 'label' => $this->faker->word, 28 | 'placeholder' => $this->faker->word, 29 | 'model' => $this->faker->word, 30 | 'required' => $this->faker->boolean(10), 31 | 'validation_id' => null, 32 | 'group' => null, 33 | 'order' => null, 34 | 'created_at' => now(), 35 | 'updated_at' => now(), 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Database/Factories/FormFactory.php: -------------------------------------------------------------------------------- 1 | implode('_', $this->faker->words(5)), 25 | 'definition' => json_encode(['test' => 'test']), 26 | 'created_at' => now(), 27 | 'updated_at' => now(), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Database/Factories/PlainTypeFactory.php: -------------------------------------------------------------------------------- 1 | implode(' ', $this->faker->words(5)), 25 | 'created_at' => now(), 26 | 'updated_at' => now(), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Database/Factories/RelationFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->randomNumber(), 25 | 'child_id' => $this->faker->randomNumber(), 26 | 'created_at' => now(), 27 | 'updated_at' => now(), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Database/Factories/RemoteTypeFactory.php: -------------------------------------------------------------------------------- 1 | null, 25 | 'url' => $this->faker->url, 26 | 'method' => $this->faker->randomElement(['GET', 'POST', 'PUT']), 27 | 'body' => '{"test":"test"}', 28 | 'mappings' => json_encode( 29 | array_combine( 30 | $this->faker->words(5), 31 | $this->faker->words(5), 32 | )), 33 | 'created_at' => now(), 34 | 'updated_at' => now(), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Database/Factories/SelectionTypeFactory.php: -------------------------------------------------------------------------------- 1 | null, 25 | 'multiselect' => $this->faker->boolean, 26 | 'created_at' => now(), 27 | 'updated_at' => now(), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Database/Factories/SelectionValueFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->randomNumber(), 25 | 'label' => $this->faker->word, 26 | 'preselect' => $this->faker->boolean(10), 27 | 'value' => $this->faker->word, 28 | 'created_at' => now(), 29 | 'updated_at' => now(), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Database/Factories/ValidationFactory.php: -------------------------------------------------------------------------------- 1 | implode(' ', $this->faker->words(5)), 25 | 'regex' => 'some_regex', 26 | 'generic' => $this->faker->boolean(10), 27 | 'created_at' => now(), 28 | 'updated_at' => now(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Database/Factories/ValueFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->word, 25 | 'model_id' => $this->faker->randomNumber(), 26 | 'custom_field_id' => $this->faker->randomNumber(), 27 | 'integer' => null, 28 | 'float' => null, 29 | 'date' => null, 30 | 'time' => null, 31 | 'datetime' => null, 32 | 'text' => null, 33 | 'boolean' => null, 34 | 'string' => null, 35 | 'created_at' => now(), 36 | 'updated_at' => now(), 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Database/Seeders/CustomFieldFormSeeder.php: -------------------------------------------------------------------------------- 1 | isEmpty()) { 24 | echo "No custom fields available, skipping...\n"; 25 | 26 | return; 27 | } 28 | 29 | foreach ($forms as $form) { 30 | $rand = rand(1, 10); 31 | 32 | $ids = []; 33 | for ($i = 0; $i < $rand; $i++) { 34 | $ids[] = $customFields->random(1)->first()->id; 35 | } 36 | 37 | $form->customFields()->sync($ids); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Database/Seeders/CustomFieldPackageSeeder.php: -------------------------------------------------------------------------------- 1 | call([ 14 | RemoteTypeSeeder::class, 15 | SelectionTypeSeeder::class, 16 | SelectionValueSeeder::class, 17 | 18 | ValidationSeeder::class, 19 | CustomFieldSeeder::class, 20 | RelationSeeder::class, 21 | 22 | ValueSeeder::class, 23 | 24 | FormSeeder::class, 25 | CustomFieldFormSeeder::class, 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Database/Seeders/CustomFieldSeeder.php: -------------------------------------------------------------------------------- 1 | getModelsWithTrait($traitPath); 29 | 30 | if (!$models) { 31 | echo "No models with Customizable trait available, skipping...\n"; 32 | 33 | return; 34 | } 35 | 36 | /** @var CustomField $customField */ 37 | $customField = app(CustomField::class); 38 | /** @var PlainType $plainType */ 39 | $plainType = app(PlainType::class); 40 | /** @var SelectionType $selectionType */ 41 | $selectionType = app(SelectionType::class); 42 | /** @var RemoteType $remoteType */ 43 | $remoteType = app(RemoteType::class); 44 | /** @var Validation $validation */ 45 | $validation = app(Validation::class); 46 | 47 | $types = $customField::types(); 48 | $plainTypes = $plainType::all('id', 'name'); 49 | $selectionTypes = $selectionType::all('id'); 50 | $remoteTypes = $remoteType::all('id'); 51 | $validations = $validation::all('id'); 52 | 53 | for ($j = 0; $j < 20; $j++) { 54 | $customFields = $customField::factory()->count(10)->make() 55 | ->each(function (CustomField $customField) use ($types, $plainTypes, $selectionTypes, $remoteTypes, $validations, $models) { 56 | if (config('asseco-custom-fields.migrations.uuid')) { 57 | $customField->id = Str::uuid(); 58 | } 59 | 60 | $typeName = array_rand($types); 61 | $typeClass = $types[$typeName]; 62 | $typeValue = $this->getTypeValue($typeClass, $typeName, $plainTypes, $selectionTypes, $remoteTypes); 63 | $shouldValidate = $this->shouldValidate($typeClass::find($typeValue)); 64 | 65 | $customField->timestamps = false; 66 | $customField->selectable_type = $typeClass; 67 | $customField->selectable_id = $typeValue; 68 | $customField->model = $models[array_rand($models)]; 69 | $customField->validation_id = $shouldValidate ? $validations->random(1)->first()->id : null; 70 | })->toArray(); 71 | 72 | $customField::query()->insert($customFields); 73 | } 74 | } 75 | 76 | protected function getTypeValue(string $typeClass, string $typeName, Collection $plainTypes, Collection $selectionTypes, Collection $remoteTypes) 77 | { 78 | switch ($typeClass) { 79 | case config('asseco-custom-fields.models.remote_type'): 80 | return $remoteTypes->random(1)->first()->id; 81 | case config('asseco-custom-fields.models.selection_type'): 82 | return $selectionTypes->random(1)->first()->id; 83 | default: 84 | return $plainTypes->where('name', $typeName)->first()->id; 85 | } 86 | } 87 | 88 | private function shouldValidate(Model $model) 89 | { 90 | /** @var Value $value */ 91 | $value = app(Value::class); 92 | 93 | $column = $value::FALLBACK_VALUE_COLUMN; 94 | 95 | if ($model instanceof Mappable) { 96 | $column = $model::mapToValueColumn(); 97 | } elseif ($model instanceof ParentType) { 98 | /** 99 | * @var Mappable $mappable 100 | */ 101 | $mappable = $model->subTypeClassPath(); 102 | $column = $mappable::mapToValueColumn(); 103 | } 104 | 105 | return in_array($column, ['string', 'text']); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Database/Seeders/FormSeeder.php: -------------------------------------------------------------------------------- 1 | count(100)->make() 19 | ->each(function (Form $form) { 20 | if (config('asseco-custom-fields.migrations.uuid')) { 21 | $form->id = Str::uuid(); 22 | } 23 | 24 | $form->timestamps = false; 25 | }) 26 | ->toArray(); 27 | 28 | $form::query()->insert($forms); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Database/Seeders/PlainTypeSeeder.php: -------------------------------------------------------------------------------- 1 | Str::uuid(), 25 | 'name' => $type, 26 | ]; 27 | } else { 28 | $plainTypes[] = ['name' => $type]; 29 | } 30 | } 31 | 32 | $plainType::query()->upsert($plainTypes, ['name'], ['name']); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Database/Seeders/RelationSeeder.php: -------------------------------------------------------------------------------- 1 | isEmpty()) { 24 | echo "No custom fields available, skipping...\n"; 25 | 26 | return; 27 | } 28 | 29 | $relations = $relation::factory()->count(200)->make() 30 | ->each(function (Relation $relation) use ($customFields) { 31 | if (config('asseco-custom-fields.migrations.uuid')) { 32 | $relation->id = Str::uuid(); 33 | } 34 | 35 | $relation->timestamps = false; 36 | $relation->parent_id = $customFields->random(1)->first()->id; 37 | $relation->child_id = $customFields->random(1)->first()->id; 38 | })->toArray(); 39 | 40 | $relation::query()->insert($relations); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Database/Seeders/RemoteTypeSeeder.php: -------------------------------------------------------------------------------- 1 | where('name', 'string')->firstOrFail()->id; 25 | 26 | $remoteTypes = $remoteType::factory()->count(50)->make() 27 | ->each(function (RemoteType $remoteType) use ($plainTypeId, $methods) { 28 | if (config('asseco-custom-fields.migrations.uuid')) { 29 | $remoteType->id = Str::uuid(); 30 | } 31 | 32 | $remoteType->timestamps = false; 33 | $remoteType->plain_type_id = $plainTypeId; 34 | $remoteType->method = $methods[array_rand($methods)]; 35 | })->makeHidden('name')->toArray(); 36 | 37 | $remoteType::query()->insert($remoteTypes); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Database/Seeders/SelectionTypeSeeder.php: -------------------------------------------------------------------------------- 1 | count(50)->make() 24 | ->each(function (SelectionType $selectionType) use ($types) { 25 | if (config('asseco-custom-fields.migrations.uuid')) { 26 | $selectionType->id = Str::uuid(); 27 | } 28 | 29 | $selectionType->timestamps = false; 30 | $selectionType->plain_type_id = $types->random(1)->first()->id; 31 | })->makeHidden('name')->toArray(); 32 | 33 | $selectionType::query()->insert($selectionTypes); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Database/Seeders/SelectionValueSeeder.php: -------------------------------------------------------------------------------- 1 | whereDoesntHave('values')->get(); 28 | 29 | $selectionValues = []; 30 | foreach ($selectionTypes as $selectionType) { 31 | // Merging to enable single insert for performance reasons 32 | $selectionValues = array_merge_recursive($selectionValues, 33 | $this->makeValue($selectionValueClass, $selectionType, $faker) 34 | ); 35 | 36 | // We do this to reset fakers unique value list, so that 37 | // values are unique per custom field instead of database 38 | $faker->unique(true); 39 | } 40 | 41 | // When comparing type name, this gets appended to the model 42 | // which breaks the insert if not removed 43 | foreach ($selectionValues as &$selectionValue) { 44 | unset($selectionValue['type']); 45 | } 46 | 47 | $selectionValueClass::query()->insert($selectionValues); 48 | } 49 | 50 | protected function makeValue(SelectionValue $selectionValueClass, SelectionType $selectionType, Generator $faker): array 51 | { 52 | // Have random number of values set for a single type, 2 for boolean plain type 53 | $number = $selectionType->type->name === 'boolean' ? 2 : rand(3, 10); 54 | 55 | return $selectionValueClass::factory()->count($number)->make() 56 | ->each(function (SelectionValue $selectionValue) use ($selectionType, $faker) { 57 | if (config('asseco-custom-fields.migrations.uuid')) { 58 | $selectionValue->id = Str::uuid()->toString(); 59 | } 60 | 61 | $selectionValue->timestamps = false; 62 | $selectionValue->selection_type_id = $selectionType->id; 63 | $selectionValue->value = $this->fakeValueFromType($selectionType->type->name, $faker); 64 | })->toArray(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Database/Seeders/ValidationSeeder.php: -------------------------------------------------------------------------------- 1 | count(200)->make([ 19 | 'id' => function () { 20 | if (config('asseco-custom-fields.migrations.uuid')) { 21 | return Str::uuid(); 22 | } 23 | 24 | return null; 25 | }, 26 | ])->each(function (Validation $validation) { 27 | $validation->timestamps = false; 28 | })->toArray(); 29 | 30 | $validation::query()->insert($validations); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Database/Seeders/ValueSeeder.php: -------------------------------------------------------------------------------- 1 | getModelsWithTrait($traitPath); 35 | $customFields = $customFieldClass::all(); 36 | 37 | if ($customFields->isEmpty()) { 38 | echo "No custom fields available, skipping...\n"; 39 | 40 | return; 41 | } 42 | 43 | $values = $valueClass::factory()->count(500)->make() 44 | ->each(function (Value $value) use ($customFields, $models, $faker) { 45 | if (config('asseco-custom-fields.migrations.uuid')) { 46 | $value->id = Str::uuid(); 47 | } 48 | 49 | $customField = $customFields->random(1)->first(); 50 | $model = $models[array_rand($models)]; 51 | $selectable = $customField->selectable; 52 | [$type, $typeValue] = $this->getType($selectable); 53 | $fakeValue = $typeValue ?: $this->fakeValueFromType($type, $faker); 54 | 55 | $value->timestamps = false; 56 | $value->model_type = $model; 57 | $value->model_id = $this->getCached($model); 58 | $value->custom_field_id = $customField->id; 59 | $value->{$type} = $fakeValue; 60 | })->makeHidden('value')->toArray(); 61 | 62 | $valueClass::query()->insert($values); 63 | } 64 | 65 | protected function getCached(string $model): string 66 | { 67 | if (!array_key_exists($model, $this->cached)) { 68 | /** @var Model $model */ 69 | $this->cached[$model] = $model::all('id')->pluck('id')->toArray(); 70 | } 71 | 72 | $cached = $this->cached[$model]; 73 | 74 | return $cached[array_rand($cached)]; 75 | } 76 | 77 | protected function getType($selectable): array 78 | { 79 | if ($selectable instanceof SelectionType) { 80 | return [$selectable->type->name, $selectable->values->random(1)->first()->value]; 81 | } elseif ($selectable instanceof RemoteType) { 82 | return ['string', null]; 83 | } else { 84 | return [$selectable->name, null]; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Feature/Http/Controllers/CustomFieldControllerTest.php: -------------------------------------------------------------------------------- 1 | customField = app(CustomField::class); 19 | } 20 | 21 | /** @test */ 22 | public function can_fetch_all_custom_fields() 23 | { 24 | $this 25 | ->getJson(route('custom-fields.index')) 26 | ->assertJsonCount(0); 27 | 28 | $this->customField::factory()->count(5)->create(); 29 | 30 | $this 31 | ->getJson(route('custom-fields.index')) 32 | ->assertJsonCount(5); 33 | 34 | $this->assertCount(5, $this->customField::all()); 35 | } 36 | 37 | /** @test */ 38 | public function rejects_creating_custom_field_with_invalid_name() 39 | { 40 | $request = $this->customField::factory()->make([ 41 | 'name' => 'invalid name', 42 | ])->toArray(); 43 | 44 | $this 45 | ->postJson(route('custom-fields.store'), $request) 46 | ->assertStatus(422); 47 | } 48 | 49 | /** @test */ 50 | public function creates_custom_field() 51 | { 52 | $request = $this->customField::factory()->make()->toArray(); 53 | 54 | $this 55 | ->postJson(route('custom-fields.store'), $request) 56 | ->assertJsonFragment([ 57 | 'name' => $request['name'], 58 | ]); 59 | 60 | $this->assertCount(1, $this->customField::all()); 61 | } 62 | 63 | /** @test */ 64 | public function can_return_custom_field_by_id() 65 | { 66 | $customFields = $this->customField::factory()->count(5)->create(); 67 | 68 | $customFieldId = $customFields->random()->id; 69 | 70 | $this 71 | ->getJson(route('custom-fields.show', $customFieldId)) 72 | ->assertJsonFragment(['id' => $customFieldId]); 73 | } 74 | 75 | /** @test */ 76 | public function can_update_custom_field() 77 | { 78 | $customField = $this->customField::factory()->create(); 79 | 80 | $request = [ 81 | 'name' => 'updated_name', 82 | ]; 83 | 84 | $this 85 | ->putJson(route('custom-fields.update', $customField->id), $request) 86 | ->assertJsonFragment([ 87 | 'name' => $request['name'], 88 | ]); 89 | 90 | $this->assertEquals($request['name'], $customField->refresh()->name); 91 | } 92 | 93 | /** @test */ 94 | public function can_delete_custom_field() 95 | { 96 | $customField = $this->customField::factory()->create(); 97 | 98 | $this->assertCount(1, $this->customField::all()); 99 | 100 | $this 101 | ->deleteJson(route('custom-fields.destroy', $customField->id)) 102 | ->assertOk(); 103 | 104 | $this->assertCount(0, $this->customField::all()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Feature/Http/Controllers/FormControllerTest.php: -------------------------------------------------------------------------------- 1 | form = app(Form::class); 19 | } 20 | 21 | /** @test */ 22 | public function can_fetch_all_forms() 23 | { 24 | $this 25 | ->getJson(route('custom-field.forms.index')) 26 | ->assertJsonCount(0); 27 | 28 | $this->form::factory()->count(5)->create(); 29 | 30 | $this 31 | ->getJson(route('custom-field.forms.index')) 32 | ->assertJsonCount(5); 33 | 34 | $this->assertCount(5, $this->form::all()); 35 | } 36 | 37 | /** @test */ 38 | public function rejects_creating_form_with_invalid_name() 39 | { 40 | $request = $this->form::factory()->make([ 41 | 'name' => 'invalid name', 42 | ])->toArray(); 43 | 44 | $this 45 | ->postJson(route('custom-field.forms.store'), $request) 46 | ->assertStatus(422); 47 | } 48 | 49 | /** @test */ 50 | public function creates_form() 51 | { 52 | $request = $this->form::factory()->make([ 53 | 'definition' => ['a' => 'b'], 54 | ])->toArray(); 55 | 56 | $this 57 | ->postJson(route('custom-field.forms.store'), $request) 58 | ->assertJsonFragment([ 59 | 'name' => $request['name'], 60 | ]); 61 | 62 | $this->assertCount(1, $this->form::all()); 63 | } 64 | 65 | /** @test */ 66 | public function can_return_form_by_id() 67 | { 68 | $forms = $this->form::factory()->count(5)->create(); 69 | 70 | $formId = $forms->random()->id; 71 | 72 | $this 73 | ->getJson(route('custom-field.forms.show', $formId)) 74 | ->assertJsonFragment(['id' => $formId]); 75 | } 76 | 77 | /** @test */ 78 | public function can_update_form() 79 | { 80 | $form = $this->form::factory()->create(); 81 | 82 | $request = [ 83 | 'name' => 'updated_name', 84 | ]; 85 | 86 | $this 87 | ->putJson(route('custom-field.forms.update', $form->id), $request) 88 | ->assertJsonFragment([ 89 | 'name' => $request['name'], 90 | ]); 91 | 92 | $this->assertEquals($request['name'], $form->refresh()->name); 93 | } 94 | 95 | /** @test */ 96 | public function can_delete_form() 97 | { 98 | $form = $this->form::factory()->create(); 99 | 100 | $this->assertCount(1, $this->form::all()); 101 | 102 | $this 103 | ->deleteJson(route('custom-field.forms.destroy', $form->id)) 104 | ->assertOk(); 105 | 106 | $this->assertCount(0, $this->form::all()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/Feature/Http/Controllers/PlainCustomFieldControllerTest.php: -------------------------------------------------------------------------------- 1 | customField = app(CustomField::class); 23 | $this->plainType = app(PlainType::class); 24 | } 25 | 26 | /** @test */ 27 | public function returns_only_plain_custom_fields() 28 | { 29 | $plainType1 = $this->plainType::query()->where('name', 'string')->first(); 30 | $plainType2 = $this->plainType::query()->where('name', 'boolean')->first(); 31 | 32 | $this->customField::factory()->create([ 33 | 'selectable_type' => StringType::class, 34 | 'selectable_id' => $plainType1->id, 35 | ]); 36 | $this->customField::factory()->create([ 37 | 'selectable_type' => BooleanType::class, 38 | 'selectable_id' => $plainType2->id, 39 | ]); 40 | 41 | $this->customField::factory()->count(5)->create(); 42 | 43 | $this 44 | ->getJson(route('custom-field.plain.index')) 45 | ->assertJsonCount(2); 46 | 47 | $this->assertCount(7, $this->customField::all()); 48 | } 49 | 50 | /** @test */ 51 | public function rejects_creating_plain_custom_field_with_invalid_name() 52 | { 53 | $request = $this->customField::factory()->make([ 54 | 'name' => 'invalid name', 55 | ])->toArray(); 56 | 57 | $this 58 | ->postJson(route('custom-field.plain.store', 'string'), $request) 59 | ->assertStatus(422); 60 | } 61 | 62 | /** @test */ 63 | public function creates_plain_custom_field() 64 | { 65 | $request = $this->customField::factory()->make()->toArray(); 66 | 67 | $this 68 | ->postJson(route('custom-field.plain.store', 'string'), $request) 69 | ->assertJsonFragment([ 70 | 'id' => 1, 71 | 'name' => $request['name'], 72 | ]); 73 | 74 | $this->assertCount(1, $this->customField::all()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Feature/Http/Controllers/RelationControllerTest.php: -------------------------------------------------------------------------------- 1 | customField = app(CustomField::class); 21 | $this->relation = app(Relation::class); 22 | } 23 | 24 | /** @test */ 25 | public function can_fetch_all_relations() 26 | { 27 | $this 28 | ->getJson(route('custom-field.relations.index')) 29 | ->assertJsonCount(0); 30 | 31 | $this->relation::factory()->count(5)->create(); 32 | 33 | $this 34 | ->getJson(route('custom-field.relations.index')) 35 | ->assertJsonCount(5); 36 | 37 | $this->assertCount(5, $this->relation::all()); 38 | } 39 | 40 | /** @test */ 41 | public function creates_relation() 42 | { 43 | $customFields = $this->customField::factory()->count(2)->create(); 44 | 45 | $this 46 | ->postJson(route('custom-field.relations.store'), [ 47 | 'parent_id' => $customFields->first()->id, 48 | 'child_id' => $customFields->last()->id, 49 | ]); 50 | 51 | $this->assertCount(1, $this->relation::all()); 52 | } 53 | 54 | /** @test */ 55 | public function can_return_relation_by_id() 56 | { 57 | $this->relation::factory()->count(5)->create(); 58 | 59 | $this 60 | ->getJson(route('custom-field.relations.show', 3)) 61 | ->assertJsonFragment(['id' => 3]); 62 | } 63 | 64 | /** @test */ 65 | public function can_update_relation() 66 | { 67 | $customFields = $this->customField::factory()->count(5)->create(); 68 | 69 | $cf1 = $customFields->first()->id; 70 | $cf2 = $customFields->last()->id; 71 | 72 | $relation = $this->relation::factory()->create([ 73 | 'parent_id' => $cf1, 74 | 'child_id' => $cf2, 75 | ]); 76 | 77 | $random1 = $customFields->whereNotIn('id', [$cf1, $cf2])->random()->id; 78 | $random2 = $customFields->whereNotIn('id', [$cf1, $cf2, $random1])->random()->id; 79 | 80 | $this 81 | ->putJson(route('custom-field.relations.update', $relation->id), [ 82 | 'parent_id' => $random1, 83 | ]); 84 | 85 | $this 86 | ->putJson(route('custom-field.relations.update', $relation->id), [ 87 | 'child_id' => $random2, 88 | ]); 89 | 90 | $relation->refresh(); 91 | 92 | $this->assertEquals($random1, $relation->parent_id); 93 | $this->assertEquals($random2, $relation->child_id); 94 | } 95 | 96 | /** @test */ 97 | public function can_delete_relation() 98 | { 99 | $relation = $this->relation::factory()->create(); 100 | 101 | $this->assertCount(1, $this->relation::all()); 102 | 103 | $this 104 | ->deleteJson(route('custom-field.relations.destroy', $relation->id)) 105 | ->assertOk(); 106 | 107 | $this->assertCount(0, $this->relation::all()); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/Feature/Http/Controllers/RemoteCustomFieldControllerTest.php: -------------------------------------------------------------------------------- 1 | customField = app(CustomField::class); 23 | $this->plainType = app(PlainType::class); 24 | $this->remoteType = app(RemoteType::class); 25 | } 26 | 27 | /** @test */ 28 | public function returns_only_remote_custom_fields() 29 | { 30 | $this 31 | ->getJson(route('custom-field.remote.index')) 32 | ->assertJsonCount(0); 33 | 34 | $remoteType = $this->remoteType::factory()->create([ 35 | 'plain_type_id' => $this->plainType::query()->where('name', 'string')->first()->id, 36 | ]); 37 | 38 | $this->customField::factory()->create([ 39 | 'selectable_type' => get_class($this->remoteType), 40 | 'selectable_id' => $remoteType->id, 41 | ]); 42 | 43 | $this->customField::factory()->count(5)->create(); 44 | 45 | $this 46 | ->getJson(route('custom-field.remote.index')) 47 | ->assertJsonCount(1); 48 | 49 | $this->assertCount(6, $this->customField::all()); 50 | } 51 | 52 | /** @test */ 53 | public function rejects_creating_remote_custom_field_with_invalid_name() 54 | { 55 | $request = $this->customField::factory()->make([ 56 | 'name' => 'invalid name', 57 | ])->toArray(); 58 | 59 | $this 60 | ->postJson(route('custom-field.remote.store'), $request) 61 | ->assertStatus(422); 62 | } 63 | 64 | /** @test */ 65 | public function rejects_creating_remote_custom_field_without_remote_parameters() 66 | { 67 | $request = $this->customField::factory()->make()->toArray(); 68 | 69 | $this 70 | ->postJson(route('custom-field.remote.store'), $request) 71 | ->assertStatus(422); 72 | } 73 | 74 | /** @test */ 75 | public function creates_remote_custom_field() 76 | { 77 | $request = $this->customField::factory()->make()->toArray(); 78 | 79 | $request['remote'] = $this->remoteType::factory()->make([ 80 | 'plain_type_id' => $this->plainType::query()->where('name', 'string')->first()->id, 81 | 'body' => [], 82 | 'mappings' => [], 83 | ])->toArray(); 84 | 85 | $this 86 | ->postJson(route('custom-field.remote.store'), $request) 87 | ->assertJsonFragment([ 88 | 'name' => $request['name'], 89 | ]); 90 | 91 | $this->assertCount(1, $this->customField::all()); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Feature/Http/Controllers/SelectionCustomFieldControllerTest.php: -------------------------------------------------------------------------------- 1 | customField = app(CustomField::class); 25 | $this->plainType = app(PlainType::class); 26 | $this->selectionType = app(SelectionType::class); 27 | $this->selectionValue = app(SelectionValue::class); 28 | } 29 | 30 | /** @test */ 31 | public function returns_only_selection_custom_fields() 32 | { 33 | $this 34 | ->getJson(route('custom-field.selection.index')) 35 | ->assertJsonCount(0); 36 | 37 | $selectionType = $this->selectionType::factory()->create([ 38 | 'plain_type_id' => $this->plainType::query()->where('name', 'string')->first()->id, 39 | ]); 40 | 41 | $this->customField::factory()->create([ 42 | 'selectable_type' => get_class($this->selectionType), 43 | 'selectable_id' => $selectionType->id, 44 | ]); 45 | 46 | $this->customField::factory()->count(5)->create(); 47 | 48 | $this 49 | ->getJson(route('custom-field.selection.index')) 50 | ->assertJsonCount(1); 51 | 52 | $this->assertCount(6, $this->customField::all()); 53 | } 54 | 55 | /** @test */ 56 | public function rejects_creating_selection_custom_field_with_invalid_name() 57 | { 58 | $request = $this->customField::factory()->make([ 59 | 'name' => 'invalid name', 60 | ])->toArray(); 61 | 62 | $this 63 | ->postJson(route('custom-field.selection.store', 'string'), $request) 64 | ->assertStatus(422); 65 | } 66 | 67 | /** @test */ 68 | public function creates_selection_custom_field() 69 | { 70 | $request = $this->customField::factory()->make()->toArray(); 71 | 72 | $request['selection'] = $this->selectionType::factory()->make([ 73 | 'plain_type_id' => $this->plainType::query()->where('name', 'string')->first()->id, 74 | ])->toArray(); 75 | 76 | $this 77 | ->postJson(route('custom-field.selection.store', 'string'), $request) 78 | ->assertJsonFragment([ 79 | 'id' => 1, 80 | 'name' => $request['name'], 81 | ]); 82 | 83 | $this->assertCount(1, $this->customField::all()); 84 | } 85 | 86 | /** @test */ 87 | public function creates_selection_custom_field_with_values() 88 | { 89 | $request = $this->customField::factory()->make()->toArray(); 90 | 91 | $request['selection'] = $this->selectionType::factory()->make([ 92 | 'plain_type_id' => $this->plainType::query()->where('name', 'string')->first()->id, 93 | ])->toArray(); 94 | 95 | // TODO: flaky test because 'value' is not unique in factory. If set to unique, it breaks seeders 96 | $request['values'] = $this->selectionValue::factory()->count(5)->make()->toArray(); 97 | 98 | $this 99 | ->postJson(route('custom-field.selection.store', 'string'), $request) 100 | ->assertJsonFragment([ 101 | 'id' => 1, 102 | 'name' => $request['name'], 103 | ]); 104 | 105 | $this->assertCount(1, $this->customField::all()); 106 | $this->assertCount(5, $this->selectionValue::all()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/Feature/Http/Controllers/SelectionValueControllerTest.php: -------------------------------------------------------------------------------- 1 | plainType = app(PlainType::class); 23 | $this->selectionType = app(SelectionType::class); 24 | $this->selectionValue = app(SelectionValue::class); 25 | } 26 | 27 | /** @test */ 28 | public function can_fetch_all_selection_values() 29 | { 30 | $this 31 | ->getJson(route('custom-field.selection-values.index')) 32 | ->assertJsonCount(0); 33 | 34 | $this->selectionValue::factory()->count(5)->create(); 35 | 36 | $this 37 | ->getJson(route('custom-field.selection-values.index')) 38 | ->assertJsonCount(5); 39 | 40 | $this->assertCount(5, $this->selectionValue::all()); 41 | } 42 | 43 | /** @test */ 44 | public function creates_selection_value() 45 | { 46 | $plain = $this->plainType::factory()->create(); 47 | 48 | $type = $this->selectionType::factory()->create([ 49 | 'plain_type_id' => $plain->id, 50 | ]); 51 | 52 | $request = $this->selectionValue::factory()->make([ 53 | 'selection_type_id' => $type->id, 54 | ])->toArray(); 55 | 56 | $this 57 | ->postJson(route('custom-field.selection-values.store'), $request) 58 | ->assertJsonFragment([ 59 | 'label' => $request['label'], 60 | ]); 61 | 62 | $this->assertCount(1, $this->selectionValue::all()); 63 | } 64 | 65 | /** @test */ 66 | public function can_return_selection_value_by_id() 67 | { 68 | $selectionValues = $this->selectionValue::factory()->count(5)->create(); 69 | 70 | $selectionValueId = $selectionValues->random()->id; 71 | 72 | $this 73 | ->getJson(route('custom-field.selection-values.show', $selectionValueId)) 74 | ->assertJsonFragment(['id' => $selectionValueId]); 75 | } 76 | 77 | /** @test */ 78 | public function can_update_selection_value() 79 | { 80 | $plain = $this->plainType::factory()->create(); 81 | 82 | $type = $this->selectionType::factory()->create([ 83 | 'plain_type_id' => $plain->id, 84 | ]); 85 | 86 | $selectionValue = $this->selectionValue::factory()->create([ 87 | 'selection_type_id' => $type->id, 88 | ]); 89 | 90 | $request = [ 91 | 'label' => 'updated_label', 92 | ]; 93 | 94 | $this 95 | ->putJson(route('custom-field.selection-values.update', $selectionValue->id), $request) 96 | ->assertJsonFragment([ 97 | 'label' => $request['label'], 98 | ]); 99 | 100 | $this->assertEquals($request['label'], $selectionValue->refresh()->label); 101 | } 102 | 103 | /** @test */ 104 | public function can_delete_selection_value() 105 | { 106 | $selectionValue = $this->selectionValue::factory()->create(); 107 | 108 | $this->assertCount(1, $this->selectionValue::all()); 109 | 110 | $this 111 | ->deleteJson(route('custom-field.selection-values.destroy', $selectionValue->id)) 112 | ->assertOk(); 113 | 114 | $this->assertCount(0, $this->selectionValue::all()); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/Feature/Http/Controllers/ValidationControllerTest.php: -------------------------------------------------------------------------------- 1 | validation = app(Validation::class); 19 | } 20 | 21 | /** @test */ 22 | public function can_fetch_all_validations() 23 | { 24 | $this 25 | ->getJson(route('custom-field.validations.index')) 26 | ->assertJsonCount(0); 27 | 28 | $this->validation::factory()->count(5)->create(); 29 | 30 | $this 31 | ->getJson(route('custom-field.validations.index')) 32 | ->assertJsonCount(5); 33 | 34 | $this->assertCount(5, $this->validation::all()); 35 | } 36 | 37 | /** @test */ 38 | public function creates_validation() 39 | { 40 | $request = $this->validation::factory()->make()->toArray(); 41 | 42 | $this 43 | ->postJson(route('custom-field.validations.store'), $request) 44 | ->assertJsonFragment([ 45 | 'name' => $request['name'], 46 | ]); 47 | 48 | $this->assertCount(1, $this->validation::all()); 49 | } 50 | 51 | /** @test */ 52 | public function can_return_validation_by_id() 53 | { 54 | $validations = $this->validation::factory()->count(5)->create(); 55 | 56 | $validationId = $validations->random()->id; 57 | 58 | $this 59 | ->getJson(route('custom-field.validations.show', $validationId)) 60 | ->assertJsonFragment(['id' => $validationId]); 61 | } 62 | 63 | /** @test */ 64 | public function can_update_validation() 65 | { 66 | $validation = $this->validation::factory()->create(); 67 | 68 | $request = [ 69 | 'name' => 'updated_name', 70 | ]; 71 | 72 | $this 73 | ->putJson(route('custom-field.validations.update', $validation->id), $request) 74 | ->assertJsonFragment([ 75 | 'name' => $request['name'], 76 | ]); 77 | 78 | $this->assertEquals($request['name'], $validation->refresh()->name); 79 | } 80 | 81 | /** @test */ 82 | public function can_delete_validation() 83 | { 84 | $validation = $this->validation::factory()->create(); 85 | 86 | $this->assertCount(1, $this->validation::all()); 87 | 88 | $this 89 | ->deleteJson(route('custom-field.validations.destroy', $validation->id)) 90 | ->assertOk(); 91 | 92 | $this->assertCount(0, $this->validation::all()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Feature/Http/Controllers/ValueControllerTest.php: -------------------------------------------------------------------------------- 1 | customField = app(CustomField::class); 24 | $this->plainType = app(PlainType::class); 25 | $this->value = app(Value::class); 26 | } 27 | 28 | /** @test */ 29 | public function can_fetch_all_values() 30 | { 31 | $this 32 | ->getJson(route('custom-field.values.index')) 33 | ->assertJsonCount(0); 34 | 35 | $customField = $this->customField::factory()->create(); 36 | 37 | $this->value::factory()->count(5)->create(['custom_field_id' => $customField->id]); 38 | 39 | $this 40 | ->getJson(route('custom-field.values.index')) 41 | ->assertJsonCount(5); 42 | 43 | $this->assertCount(5, $this->value::all()); 44 | } 45 | 46 | /** @test */ 47 | public function creates_value() 48 | { 49 | $selectable = $this->plainType::query()->where('name', 'string')->first(); 50 | $customField = $this->customField::factory()->create([ 51 | 'selectable_type' => StringType::class, 52 | 'selectable_id' => $selectable->id, 53 | ]); 54 | 55 | $request = $this->value::factory()->make([ 56 | 'custom_field_id' => $customField->id, 57 | 'string' => 'test value', 58 | ])->toArray(); 59 | 60 | $this 61 | ->postJson(route('custom-field.values.store'), $request) 62 | ->assertJsonFragment([ 63 | 'string' => $request['string'], 64 | ]); 65 | 66 | $this->assertCount(1, $this->value::all()); 67 | } 68 | 69 | /** @test */ 70 | public function fails_creating_if_value_has_no_valid_custom_field_relation() 71 | { 72 | // This way, CustomField ID will be random, not existing 73 | $request = $this->value::factory()->make()->toArray(); 74 | 75 | $this 76 | ->postJson(route('custom-field.values.store'), $request) 77 | ->assertStatus(422); 78 | 79 | $this->assertCount(0, $this->value::all()); 80 | } 81 | 82 | /** @test */ 83 | public function fails_creating_if_value_has_inadequate_value_types() 84 | { 85 | $selectable = $this->plainType::query()->where('name', 'string')->first(); 86 | $customField = $this->customField::factory()->create([ 87 | 'selectable_type' => StringType::class, 88 | 'selectable_id' => $selectable->id, 89 | ]); 90 | 91 | // Value is defined as 'string', so no other types should be provided 92 | $request = $this->value::factory()->make([ 93 | 'custom_field_id' => $customField->id, 94 | 'string' => 'test value', 95 | 'text' => 'should not be provided', 96 | ])->toArray(); 97 | 98 | $this 99 | ->postJson(route('custom-field.values.store'), $request) 100 | ->assertStatus(500); 101 | 102 | $this->assertCount(0, $this->value::all()); 103 | } 104 | 105 | /** @test */ 106 | public function fails_creating_if_value_has_missing_value_types() 107 | { 108 | $selectable = $this->plainType::query()->where('name', 'string')->first(); 109 | $customField = $this->customField::factory()->create([ 110 | 'selectable_type' => StringType::class, 111 | 'selectable_id' => $selectable->id, 112 | ]); 113 | 114 | // 'string' is required, but missing 115 | $request = $this->value::factory()->make([ 116 | 'custom_field_id' => $customField->id, 117 | ])->toArray(); 118 | 119 | $this 120 | ->postJson(route('custom-field.values.store'), $request) 121 | ->assertStatus(500); 122 | 123 | $this->assertCount(0, $this->value::all()); 124 | } 125 | 126 | /** @test */ 127 | public function can_return_value_by_id() 128 | { 129 | $customField = $this->customField::factory()->create(); 130 | 131 | $this->value::factory()->count(5)->create(['custom_field_id' => $customField->id]); 132 | 133 | $this 134 | ->getJson(route('custom-field.values.show', 3)) 135 | ->assertJsonFragment(['id' => 3]); 136 | } 137 | 138 | /** @test */ 139 | public function can_update_value() 140 | { 141 | $selectable = $this->plainType::query()->where('name', 'string')->first(); 142 | $customField = $this->customField::factory()->create([ 143 | 'selectable_type' => StringType::class, 144 | 'selectable_id' => $selectable->id, 145 | ]); 146 | 147 | $value = $this->value::factory()->create([ 148 | 'custom_field_id' => $customField->id, 149 | 'string' => 'test value', 150 | ]); 151 | 152 | $request = [ 153 | 'string' => 'updated_value', 154 | ]; 155 | 156 | $this 157 | ->putJson(route('custom-field.values.update', $value->id), $request) 158 | ->assertJsonFragment([ 159 | 'string' => $request['string'], 160 | ]); 161 | 162 | $this->assertEquals($request['string'], $value->refresh()->string); 163 | } 164 | 165 | /** @test */ 166 | public function fails_updating_if_value_has_inadequate_value_types() 167 | { 168 | $selectable = $this->plainType::query()->where('name', 'string')->first(); 169 | $customField = $this->customField::factory()->create([ 170 | 'selectable_type' => StringType::class, 171 | 'selectable_id' => $selectable->id, 172 | ]); 173 | 174 | $value = $this->value::factory()->create([ 175 | 'custom_field_id' => $customField->id, 176 | 'string' => 'test value', 177 | ]); 178 | 179 | $request = [ 180 | 'text' => 'should not be provided', 181 | ]; 182 | 183 | $this 184 | ->putJson(route('custom-field.values.update', $value->id), $request) 185 | ->assertStatus(500); 186 | } 187 | 188 | /** @test */ 189 | public function can_delete_value() 190 | { 191 | $customField = $this->customField::factory()->create(); 192 | 193 | $value = $this->value::factory()->create(['custom_field_id' => $customField->id]); 194 | 195 | $this->assertCount(1, $this->value::all()); 196 | 197 | $this 198 | ->deleteJson(route('custom-field.values.destroy', $value->id)) 199 | ->assertOk(); 200 | 201 | $this->assertCount(0, $this->value::all()); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | runLaravelMigrations(); 15 | } 16 | 17 | protected function getPackageProviders($app) 18 | { 19 | return [CustomFieldsServiceProvider::class, CommonServiceProvider::class]; 20 | } 21 | 22 | protected function getEnvironmentSetUp($app) 23 | { 24 | // perform environment setup 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Unit/Models/PlainTypeTest.php: -------------------------------------------------------------------------------- 1 | plainType = app(PlainType::class); 26 | } 27 | 28 | /** @test */ 29 | public function has_factory() 30 | { 31 | $this->assertInstanceOf(PlainTypeFactory::class, $this->plainType::factory()); 32 | } 33 | 34 | /** @test */ 35 | public function has_basic_sub_types() 36 | { 37 | $plainMappings = $this->plainType::subTypes(); 38 | 39 | $this->assertNotNull($plainMappings); 40 | 41 | $this->assertArrayHasKey('string', $plainMappings); 42 | $this->assertArrayHasKey('integer', $plainMappings); 43 | $this->assertArrayHasKey('float', $plainMappings); 44 | $this->assertArrayHasKey('date', $plainMappings); 45 | $this->assertArrayHasKey('text', $plainMappings); 46 | $this->assertArrayHasKey('boolean', $plainMappings); 47 | 48 | return $plainMappings; 49 | } 50 | 51 | /** 52 | * @test 53 | * 54 | * @depends has_basic_sub_types 55 | * 56 | * @param array $plainMappings 57 | */ 58 | public function sub_types_have_registered_classes(array $plainMappings) 59 | { 60 | foreach ($plainMappings as $typeName => $typeClass) { 61 | $class = $this->plainType::getSubTypeClass($typeName); 62 | 63 | $this->assertEquals($typeClass, $class); 64 | } 65 | } 66 | 67 | /** @test */ 68 | public function throws_error_on_non_existing_class() 69 | { 70 | $this->expectException(TypeError::class); 71 | 72 | $this->plainType::getSubTypeClass(now()->timestamp); 73 | } 74 | 75 | /** 76 | * @test 77 | * 78 | * @depends has_basic_sub_types 79 | * 80 | * @param $basicSubTypes 81 | */ 82 | public function returns_pipe_delimited_sub_types(array $basicSubTypes) 83 | { 84 | $regexSubTypes = $this->plainType::getRegexSubTypes(); 85 | 86 | $this->assertEquals(implode('|', array_keys($basicSubTypes)), $regexSubTypes); 87 | } 88 | 89 | /** 90 | * @test 91 | * 92 | * @depends has_basic_sub_types 93 | * 94 | * @param array $basicSubTypes 95 | */ 96 | public function sub_types_are_scoped_correctly(array $basicSubTypes) 97 | { 98 | $this->plainType::factory()->count(5)->create(); 99 | 100 | foreach ($basicSubTypes as $typeName => $typeClass) { 101 | $plainType = $this->plainType::query()->where('name', $typeName)->first(); 102 | $instance = new $typeClass; 103 | 104 | $this->assertInstanceOf(Mappable::class, $instance); 105 | $this->assertInstanceOf(get_class($this->plainType), $instance); 106 | 107 | /** 108 | * @var $instance Model 109 | */ 110 | $subType = $instance::query()->first(); 111 | 112 | $this->assertEquals($plainType->id, $subType->id); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/Unit/Models/RelationTest.php: -------------------------------------------------------------------------------- 1 | relation = app(Relation::class); 20 | } 21 | 22 | /** @test */ 23 | public function has_factory() 24 | { 25 | $this->assertInstanceOf(RelationFactory::class, $this->relation::factory()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Unit/Models/RemoteTypeTest.php: -------------------------------------------------------------------------------- 1 | customField = app(CustomField::class); 27 | $this->plainType = app(PlainType::class); 28 | $this->remoteType = app(RemoteType::class); 29 | } 30 | 31 | /** @test */ 32 | public function has_factory() 33 | { 34 | $this->assertInstanceOf(RemoteTypeFactory::class, $this->remoteType::factory()); 35 | } 36 | 37 | /** @test */ 38 | public function can_fetch_related_custom_fields() 39 | { 40 | $remoteType = $this->createRemoteType(); 41 | 42 | $customField = $this->customField::factory()->create([ 43 | 'selectable_type' => get_class($this->remoteType), 44 | 'selectable_id' => $remoteType->id, 45 | ]); 46 | 47 | $this->assertEquals($customField->id, $remoteType->customFields->first()->id); 48 | } 49 | 50 | /** @test */ 51 | public function appends_name_attribute() 52 | { 53 | $remoteType = $this->remoteType::factory()->make(); 54 | 55 | $this->assertEquals('remote', $remoteType->name); 56 | } 57 | 58 | protected function createRemoteType() 59 | { 60 | return $this->remoteType::factory()->create([ 61 | 'plain_type_id' => $this->plainType::factory()->create()->id, 62 | ]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Unit/Models/SelectionTypeTest.php: -------------------------------------------------------------------------------- 1 | customField = app(CustomField::class); 29 | $this->plainType = app(PlainType::class); 30 | $this->selectionType = app(SelectionType::class); 31 | $this->selectionValue = app(SelectionValue::class); 32 | } 33 | 34 | /** @test */ 35 | public function has_factory() 36 | { 37 | $this->assertInstanceOf(SelectionTypeFactory::class, $this->selectionType::factory()); 38 | } 39 | 40 | /** @test */ 41 | public function can_fetch_related_custom_fields() 42 | { 43 | $selectionType = $this->createSelectionType(); 44 | 45 | $customField = $this->customField::factory()->create([ 46 | 'selectable_type' => get_class($this->selectionType), 47 | 'selectable_id' => $selectionType->id, 48 | ]); 49 | 50 | $this->assertEquals($customField->id, $selectionType->customFields->first()->id); 51 | } 52 | 53 | /** @test */ 54 | public function can_fetch_values() 55 | { 56 | $selectionType = $this->createSelectionType(); 57 | 58 | $selectionValue = $this->selectionValue::factory()->create([ 59 | 'selection_type_id' => $selectionType->id, 60 | ]); 61 | 62 | $this->assertEquals($selectionValue->id, $selectionType->values->first()->id); 63 | } 64 | 65 | /** @test */ 66 | public function appends_name_attribute() 67 | { 68 | $remoteType = $this->selectionType::factory()->make(); 69 | 70 | $this->assertEquals('selection', $remoteType->name); 71 | } 72 | 73 | protected function createSelectionType() 74 | { 75 | return $this->selectionType::factory()->create([ 76 | 'plain_type_id' => $this->plainType::factory()->create()->id, 77 | ]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Unit/Models/SelectionValueTest.php: -------------------------------------------------------------------------------- 1 | plainType = app(PlainType::class); 27 | $this->selectionType = app(SelectionType::class); 28 | $this->selectionValue = app(SelectionValue::class); 29 | } 30 | 31 | /** @test */ 32 | public function has_factory() 33 | { 34 | $this->assertInstanceOf(SelectionValueFactory::class, $this->selectionValue::factory()); 35 | } 36 | 37 | /** @test */ 38 | public function can_fetch_selection_type() 39 | { 40 | $selectionType = $this->selectionType::factory()->create([ 41 | 'plain_type_id' => $this->plainType::factory()->create()->id, 42 | ]); 43 | 44 | $selectionValue = $this->selectionValue::factory()->create([ 45 | 'selection_type_id' => $selectionType->id, 46 | ]); 47 | 48 | $this->assertEquals($selectionValue->id, $selectionType->values->first()->id); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Unit/Models/ValidationTest.php: -------------------------------------------------------------------------------- 1 | customField = app(CustomField::class); 26 | $this->validation = app(Validation::class); 27 | } 28 | 29 | /** @test */ 30 | public function has_factory() 31 | { 32 | $this->assertInstanceOf(ValidationFactory::class, $this->validation::factory()); 33 | } 34 | 35 | /** @test */ 36 | public function can_fetch_related_custom_fields() 37 | { 38 | $validation = $this->validation::factory()->create(); 39 | 40 | $customField = $this->customField::factory()->create([ 41 | 'validation_id' => $validation->id, 42 | ]); 43 | 44 | $this->assertEquals($customField->id, $validation->customFields->first()->id); 45 | } 46 | 47 | /** @test */ 48 | public function validates_regex() 49 | { 50 | $validation = $this->validation::factory()->create([ 51 | 'regex' => '[A-Z]', 52 | ]); 53 | 54 | $this->assertNull($validation->validate('ABC')); 55 | } 56 | 57 | /** @test */ 58 | public function validates_exact_regex() 59 | { 60 | $validation = $this->validation::factory()->create([ 61 | 'regex' => '123 abc', 62 | ]); 63 | 64 | $this->assertNull($validation->validate('123 abc')); 65 | } 66 | 67 | /** @test */ 68 | public function fails_regex_validation() 69 | { 70 | $this->expectException(Exception::class); 71 | 72 | $validation = $this->validation::factory()->create([ 73 | 'regex' => '[A-Z]', 74 | ]); 75 | 76 | $validation->validate('abc'); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Unit/Models/ValueTest.php: -------------------------------------------------------------------------------- 1 | customField = app(CustomField::class); 25 | $this->value = app(Value::class); 26 | } 27 | 28 | /** @test */ 29 | public function has_factory() 30 | { 31 | $this->assertInstanceOf(ValueFactory::class, $this->value::factory()); 32 | } 33 | 34 | /** @test */ 35 | public function can_fetch_related_custom_field() 36 | { 37 | $customField = $this->customField::factory()->create(); 38 | 39 | $value = $this->value::factory()->create([ 40 | 'custom_field_id' => $customField->id, 41 | ]); 42 | 43 | $this->assertEquals($customField->id, $value->customField->id); 44 | } 45 | 46 | /** @test */ 47 | public function returns_value_from_non_empty_column() 48 | { 49 | $value = $this->value::factory()->make([ 50 | 'string' => '123', 51 | ]); 52 | 53 | $this->assertEquals('123', $value->value); 54 | } 55 | 56 | /** @test */ 57 | public function fails_to_return_if_all_value_columns_are_empty() 58 | { 59 | $value = $this->value::factory()->make(); 60 | 61 | $this->assertNull($value->value); 62 | } 63 | 64 | /** @test */ 65 | public function returns_only_first_match_of_many_non_empty_columns_by_order_of_precedence() 66 | { 67 | $value = $this->value::factory()->make([ 68 | 'integer' => 321, 69 | 'string' => 'something', 70 | ]); 71 | 72 | // 'string' is defined before 'integer' in $this->value::VALUE_COLUMNS 73 | $this->assertEquals('something', $value->value); 74 | } 75 | } 76 | --------------------------------------------------------------------------------