├── .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 |
--------------------------------------------------------------------------------