diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c8d792 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +vendor/ +.idea/ +.vscode/ +node_modules/ +.DS_Store +composer.lock diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..ffa3253 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018-present Pascal Boucher + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..697fc3b --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Laravel Form Maker + +Simple package to generate laravel form easily. + +## Installation + +Requires php >=7.2.0 and laravel 5.5+ + +``` +composer require chess/laravel-form-maker + +php artisan vendor:publish --provider="Chess\FormMaker\FormMakerServiceProvider" +``` + +This is all there is to do. + +## Documentation + +To do. + +## Credits + +- [Pascal Boucher](https://github.com/pascalboucher) + +## License + +laravel-form-maker is open-sourced software licensed under the [MIT license](https://github.com/pascalboucher/laravel-form-maker/blob/master/LICENSE.md) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7fd2e82 --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "chess/laravel-form-maker", + "description": "Simple form maker module for Laravel", + "keywords": ["laravel", "form maker", "form builder", "form generator"], + "type": "library", + "support": { + "issues": "https://github.com/pascalboucher/laravel-form-maker/issues", + "source": "https://github.com/pascalboucher/laravel-form-maker" + }, + "require": { + "php": ">=7.2.0", + "ext-fileinfo": "*", + "laravel/framework": "5.5.*|5.6.*|5.7.*" + }, + "license": "MIT", + "authors": [ + { + "name": "Pascal Boucher", + "email": "pascalboocher@gmail.com" + } + ], + "autoload": { + "psr-4": { + "Chess\\FormMaker\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Chess\\FormMaker\\FormMakerServiceProvider" + ] + } + } +} diff --git a/database/migrations/create_form_builder_tables.php b/database/migrations/create_form_builder_tables.php new file mode 100644 index 0000000..7b39904 --- /dev/null +++ b/database/migrations/create_form_builder_tables.php @@ -0,0 +1,42 @@ +increments('id'); + $table->string('title'); + $table->string('description')->nullable(); + $table->json('html_properties')->nullable(); + $table->timestamps(); + }); + + Schema::create('inputs', function (Blueprint $table) { + $table->increments('id'); + $table->morphs('inputable'); + $table->string('type'); + $table->json('html_properties')->nullable(); + $table->json('rules')->nullable(); + $table->timestamps(); + }); + + Schema::create('rankings', function (Blueprint $table) { + $table->increments('id'); + $table->morphs('rankable'); + $table->json('ranks'); + $table->timestamps(); + }); + } + + public function down() + { + Schema::drop('rankings'); + Schema::drop('inputs'); + Schema::drop('forms'); + } +} diff --git a/src/FormMakerServiceProvider.php b/src/FormMakerServiceProvider.php new file mode 100644 index 0000000..8003308 --- /dev/null +++ b/src/FormMakerServiceProvider.php @@ -0,0 +1,35 @@ +publishMigration(); + + Resource::withoutWrapping(); + } + + /** + * Publish Form Maker migrations + * + * @return void + */ + protected function publishMigration(): void + { + $timestamp = date('Y_m_d_His', time()); + + $this->publishes([ + __DIR__ . '/../database/migrations/create_form_maker_tables.php' => database_path('migrations/' . $timestamp . '_create_form_maker_tables.php'), + ], 'migrations'); + } +} diff --git a/src/Http/Resources/FormCollection.php b/src/Http/Resources/FormCollection.php new file mode 100644 index 0000000..04324ef --- /dev/null +++ b/src/Http/Resources/FormCollection.php @@ -0,0 +1,21 @@ +collection->transform(function ($form) { + return new FormResource($form); + })->toArray(); + } +} diff --git a/src/Http/Resources/FormResource.php b/src/Http/Resources/FormResource.php new file mode 100644 index 0000000..a51c0c8 --- /dev/null +++ b/src/Http/Resources/FormResource.php @@ -0,0 +1,35 @@ +inputs()->sortBy('rank')); + + return [ + 'id' => $this->id, + $this->mergeWhen($this->description, [ + 'description' => $this->description, + ]), + $this->mergeWhen($inputs->collection->isNotEmpty(), [ + 'inputs' => $inputs, + ]), + $this->mergeWhen($this->html_properties, [ + 'properties' => $this->html_properties, + ]), + 'title' => $this->title, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/src/Http/Resources/InputCollection.php b/src/Http/Resources/InputCollection.php new file mode 100644 index 0000000..f51bc9d --- /dev/null +++ b/src/Http/Resources/InputCollection.php @@ -0,0 +1,21 @@ +collection->transform(function ($input) { + return new InputResource($input); + })->toArray(); + } +} diff --git a/src/Http/Resources/InputResource.php b/src/Http/Resources/InputResource.php new file mode 100644 index 0000000..b87e660 --- /dev/null +++ b/src/Http/Resources/InputResource.php @@ -0,0 +1,34 @@ +resource, 'inputs')) { + $inputs = new InputCollection($this->inputs()->sortBy('rank')); + } + + return [ + 'id' => $this->id, + $this->mergeWhen($inputs && $inputs->collection->isNotEmpty(), [ + 'inputs' => $inputs, + ]), + $this->mergeWhen($this->html_properties, [ + 'properties' => $this->html_properties, + ]), + 'type' => $this->type, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/src/Listeners/AddInRanking.php b/src/Listeners/AddInRanking.php new file mode 100644 index 0000000..c4d81b4 --- /dev/null +++ b/src/Listeners/AddInRanking.php @@ -0,0 +1,38 @@ +model = $model; + + $this->handle(); + } + + /** + * Add the new input in the rankings. + * + * @return void + */ + protected function handle(): void + { + $this->model->inputable->ranking->add($this->model->id); + } +} diff --git a/src/Listeners/AssignProperties.php b/src/Listeners/AssignProperties.php new file mode 100644 index 0000000..8f8b348 --- /dev/null +++ b/src/Listeners/AssignProperties.php @@ -0,0 +1,55 @@ +model = $model; + + $this->handle(); + } + + /** + * Handle the event. + * + * @return void + */ + protected function handle(): void + { + $this->model->type = $this->model->getClassName(); + + foreach ($this->model->assignedProperties as $property) { + if (!isset($this->model->html_properties[$property])) { + $this->setProperty($property); + } + } + } + + /** + * Set the assigned property. + * + * @param string $property + * @return void + */ + protected function setProperty(string $property): void + { + $this->model->html_properties = [$property => uniqid(sprintf('%s_', $this->model->type))]; + } +} diff --git a/src/Listeners/CreateRanking.php b/src/Listeners/CreateRanking.php new file mode 100644 index 0000000..54f6c43 --- /dev/null +++ b/src/Listeners/CreateRanking.php @@ -0,0 +1,43 @@ +model = $model; + + $this->handle(); + } + + /** + * Associate a rankings with the model. + * + * @return void + */ + protected function handle(): void + { + $ranking = new Ranking(); + + $ranking->ranks = []; + + $this->model->ranking()->save($ranking); + } +} diff --git a/src/Listeners/DeleteInputs.php b/src/Listeners/DeleteInputs.php new file mode 100644 index 0000000..6f00f44 --- /dev/null +++ b/src/Listeners/DeleteInputs.php @@ -0,0 +1,42 @@ +model = $model; + + $this->handle(); + } + + /** + * Delete the model's inputs. + * + * @return void + */ + protected function handle(): void + { + $inputs = $this->model->inputs(); + + foreach ($inputs as $input) { + $input->delete(); + } + } +} diff --git a/src/Listeners/DeleteRanking.php b/src/Listeners/DeleteRanking.php new file mode 100644 index 0000000..4c80c4e --- /dev/null +++ b/src/Listeners/DeleteRanking.php @@ -0,0 +1,38 @@ +model = $model; + + $this->handle(); + } + + /** + * Delete an associated ranking with the model. + * + * @return void + */ + protected function handle(): void + { + $this->model->ranking->delete(); + } +} diff --git a/src/Listeners/RemoveFromRanking.php b/src/Listeners/RemoveFromRanking.php new file mode 100644 index 0000000..1cdfa55 --- /dev/null +++ b/src/Listeners/RemoveFromRanking.php @@ -0,0 +1,40 @@ +model = $model; + + $this->handle(); + } + + /** + * Remove the deleted input from the rankings. + * + * @return void + */ + protected function handle(): void + { + if ($inputable = $this->model->inputable) { + $inputable->rankings->remove($this->model->id); + } + } +} diff --git a/src/Listeners/ValidateProperties.php b/src/Listeners/ValidateProperties.php new file mode 100644 index 0000000..b0191df --- /dev/null +++ b/src/Listeners/ValidateProperties.php @@ -0,0 +1,54 @@ +model = $model; + + $this->validator = new ValidatorService(); + + $this->handle(); + } + + /** + * Handle the event. + * + * @return void + * @throws \Exception + */ + protected function handle(): void + { + $validator = $this->validator->validate($this->model); + + if ($validator->fails()) { + throw new \Exception(implode(' ', $validator->errors()->all())); + } + } +} diff --git a/src/Models/Form/Form.php b/src/Models/Form/Form.php new file mode 100644 index 0000000..dbcef52 --- /dev/null +++ b/src/Models/Form/Form.php @@ -0,0 +1,80 @@ + ValidateProperties::class, + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'description', + 'title', + ]; + + /** + * Specifies the form url action. + * + * @param string $action + * @return self + */ + public function action(string $action): self + { + $this->html_properties = ['action' => $action]; + + return $this; + } + + /** + * Specifies the form http method. + * + * @param string $method + * @return self + */ + public function method(string $method): self + { + $this->html_properties = ['method' => $method]; + + return $this; + } + + /** + * Return the form inputs rules in a form request format. + * + * @return array + * @throws \Exception + */ + public function rules(): array + { + return $this->inputs()->mapWithKeys(function ($input) { + return [$input->html_properties['name'] => implode('|', $input->rules)]; + })->all(); + } +} diff --git a/src/Models/Form/Inputs/Checkbox.php b/src/Models/Form/Inputs/Checkbox.php new file mode 100644 index 0000000..1c6aa75 --- /dev/null +++ b/src/Models/Form/Inputs/Checkbox.php @@ -0,0 +1,36 @@ + 'required', + 'html_properties.value' => 'required', + ]; + + /** + * Apply the type scope. + * + * @return void + */ + protected static function boot() + { + parent::boot(); + + static::addGlobalScope(new InputScope('checkbox')); + } +} diff --git a/src/Models/Form/Inputs/Color.php b/src/Models/Form/Inputs/Color.php new file mode 100644 index 0000000..be92af9 --- /dev/null +++ b/src/Models/Form/Inputs/Color.php @@ -0,0 +1,35 @@ + 'required', + ]; + + /** + * Apply the type scope. + * + * @return void + */ + protected static function boot() + { + parent::boot(); + + static::addGlobalScope(new InputScope('color')); + } +} diff --git a/src/Models/Form/Inputs/Datalist.php b/src/Models/Form/Inputs/Datalist.php new file mode 100644 index 0000000..bbf8413 --- /dev/null +++ b/src/Models/Form/Inputs/Datalist.php @@ -0,0 +1,35 @@ +html_properties = ['accept' => $accepted]; + + return $this; + } + + /** + * Indicates that capture of media directly from the device's sensors + * using a media capture mechanism is preferred, such as a webcam or microphone. + * + * @param string $capture + * @return self + */ + public function propertyCapture(?string $capture = 'capture'): self + { + $this->html_properties = ['capture' => $capture]; + + return $this; + } +} diff --git a/src/Models/Form/Inputs/Image.php b/src/Models/Form/Inputs/Image.php new file mode 100644 index 0000000..b0ecbcb --- /dev/null +++ b/src/Models/Form/Inputs/Image.php @@ -0,0 +1,88 @@ + 'required', + 'html_properties.src' => 'required', + ]; + + /** + * Apply the type scope. + * + * @return void + */ + protected static function boot() + { + static::addGlobalScope(new InputScope('image')); + + parent::boot(); + } + + // HTML PROPERTIES METHODS + // ============================================================== + + /** + * Specifies the alt of the input field image element. + * + * @param string $alt + * @return self + */ + public function propertyAlt(?string $alt): self + { + $this->html_properties = ['alt' => $alt]; + + return $this; + } + + /** + * Specifies the height of the input field image element. + * + * @param int $height + * @return self + */ + public function propertyHeight(?int $height = 0): self + { + $this->html_properties = ['height' => $height]; + + return $this; + } + + /** + * Specifies the src of the input field image element. + * + * @param string $src + * @return self + */ + public function propertySrc(?string $src): self + { + $this->html_properties = ['src' => $src]; + + return $this; + } + + /** + * Specifies the width of the input field image element. + * + * @param int $width + * @return self + */ + public function propertyWidth(?int $width = 0): self + { + $this->html_properties = ['width' => $width]; + + return $this; + } +} diff --git a/src/Models/Form/Inputs/Input.php b/src/Models/Form/Inputs/Input.php new file mode 100644 index 0000000..ad6428d --- /dev/null +++ b/src/Models/Form/Inputs/Input.php @@ -0,0 +1,100 @@ + 'array', + 'rules' => 'array', + ]; + + /** + * The event map for the model. + * + * @var array + */ + protected $dispatchesEvents = [ + 'created' => AddInRanking::class, + 'creating' => AssignProperties::class, + 'deleted' => RemoveFromRanking::class, + 'saving' => ValidateProperties::class, + ]; + + /** + * Set the model rules. + * + * @param array $rules + * @return void + */ + public function setRulesAttribute(array $rules): void + { + if (isset($this->attributes['rules'])) { + $rules = $this->removeNullValues( + array_merge($this->rules, $rules) + ); + } + $this->attributes['rules'] = json_encode($rules); + } + + // ELOQUENT RELATIONSHIPS + // ============================================================== + + /** + * Get the form who owns this input. + * Alias of inputable. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphTo + */ + public function form(): MorphTo + { + return $this->inputable(); + } + + /** + * Get the model who owns this input. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphTo + */ + public function inputable(): MorphTo + { + return $this->morphTo(); + } +} diff --git a/src/Models/Form/Inputs/Month.php b/src/Models/Form/Inputs/Month.php new file mode 100644 index 0000000..6bf0dbf --- /dev/null +++ b/src/Models/Form/Inputs/Month.php @@ -0,0 +1,27 @@ + 'required', + 'html_properties.value' => 'required', + ]; + + /** + * The properties automatically assigned on creation. + * + * @var array + */ + public $assignedProperties = []; + + /** + * Apply the type scope. + * + * @return void + */ + protected static function boot() + { + parent::boot(); + + static::addGlobalScope(new InputScope('option')); + } + + // HTML PROPERTIES METHODS + // ============================================================== + + /** + * Specifies the selected attribute to the option. + * + * @param string $selected + * @return self + */ + public function propertySelected(?string $selected = 'selected'): self + { + $this->html_properties = ['selected' => $selected]; + + return $this; + } + + // ELOQUENT RELATIONSHIPS + // ============================================================== + + /** + * Get the parent owning this option. + * Alias of inputable. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphTo + */ + public function parent(): MorphTo + { + return $this->inputable(); + } +} diff --git a/src/Models/Form/Inputs/Password.php b/src/Models/Form/Inputs/Password.php new file mode 100644 index 0000000..d5d99a1 --- /dev/null +++ b/src/Models/Form/Inputs/Password.php @@ -0,0 +1,32 @@ + 'required', + 'html_properties.title' => 'required', + 'html_properties.value' => 'required', + ]; + + /** + * Apply the type scope. + * + * @return void + */ + protected static function boot() + { + parent::boot(); + + static::addGlobalScope(new InputScope('radio')); + } +} diff --git a/src/Models/Form/Inputs/Range.php b/src/Models/Form/Inputs/Range.php new file mode 100644 index 0000000..c24dd06 --- /dev/null +++ b/src/Models/Form/Inputs/Range.php @@ -0,0 +1,37 @@ + 'required', + 'html_properties.min' => 'required', + ]; + + /** + * Apply the type scope. + * + * @return void + */ + protected static function boot() + { + parent::boot(); + + static::addGlobalScope(new InputScope('range')); + } +} diff --git a/src/Models/Form/Inputs/Search.php b/src/Models/Form/Inputs/Search.php new file mode 100644 index 0000000..e65c99f --- /dev/null +++ b/src/Models/Form/Inputs/Search.php @@ -0,0 +1,33 @@ + 'required', + ]; + + /** + * The relations to eager load on every query. + * + * @var array + */ + protected $with = ['options']; + + /** + * Apply the type scope. + * + * @return void + */ + protected static function boot() + { + static::addGlobalScope(new InputScope('select')); + + parent::boot(); + } + + // ELOQUENT RELATIONSHIPS + // ============================================================== + + /** + * Get the options that belongs to the select input. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphMany + */ + public function options(): MorphMany + { + return $this->morphMany('Chess\FormMaker\Models\Inputs\Option', 'inputable'); + } +} diff --git a/src/Models/Form/Inputs/Tel.php b/src/Models/Form/Inputs/Tel.php new file mode 100644 index 0000000..2f44f0e --- /dev/null +++ b/src/Models/Form/Inputs/Tel.php @@ -0,0 +1,32 @@ +html_properties = ['cols' => $cols]; + + return $this; + } + + /** + * Specifies the visible number of lines in a text area. + * + * @param int $rows + * @return self + */ + public function propertyRows(?int $rows = 0): self + { + $this->html_properties = ['rows' => $rows]; + + return $this; + } +} diff --git a/src/Models/Form/Inputs/Time.php b/src/Models/Form/Inputs/Time.php new file mode 100644 index 0000000..cc4ff29 --- /dev/null +++ b/src/Models/Form/Inputs/Time.php @@ -0,0 +1,27 @@ + 'array', + ]; + + /** + * Mass assign rules to a property. + * + * @param string $property + * @param array $rules + * @return self + */ + public function assign(string $property, array $rules): self + { + foreach ($rules as $rule => $arguments) { + $this->assignToElement($property, $rule, $arguments); + } + return $this; + } + + /** + * Assign the rule to the property. + * + * @param string $property + * @param string $rule + * @param mixed $arguments + * @return void + */ + protected function assignToElement(string $property, string $rule, $arguments): void + { + $method = camel_case(sprintf('%s_%s', str_singular($property), $rule)); + + if (method_exists($this, $method)) { + if (is_null($arguments)) { + $this->$method(null); + } else if ($arguments === $rule) { + $this->$method(); + } else { + $this->$method(...array_wrap($arguments)); + } + } + } + + /** + * Mass removal of assign rules to a property. + * + * @param string $property + * @param array $rules + * @return self + */ + public function remove(string $property, array $rules): self + { + foreach ($rules as $rule) { + $this->assignToElement($property, $rule, null); + } + return $this; + } + + /** + * Set the model html properties. + * + * @param array $properties + * @return void + */ + public function setHtmlPropertiesAttribute(array $properties): void + { + if (isset($this->attributes['html_properties'])) { + $properties = $this->removeNullValues( + array_merge($this->html_properties, $properties) + ); + } + $this->attributes['html_properties'] = json_encode($properties); + } + + // HELPERS + // ============================================================== + + /** + * Get the model class name. + * + * @return string + * @throws \ReflectionException + */ + public function getClassName(): string + { + $class = new \ReflectionClass($this); + + return strtolower($class->getShortName()); + } + + /** + * Get the model class full path. + * + * @return string + * @throws \ReflectionException + */ + public function getModelPath(): string + { + $class = new \ReflectionClass($this); + + return $class->getName(); + } + + /** + * Remove the null values in array. + * + * @param array $items + * @return array + */ + protected function removeNullValues(array $items): array + { + return array_filter($items, function ($item) { + return !is_null($item); + }); + } +} diff --git a/src/Models/Ranking.php b/src/Models/Ranking.php new file mode 100644 index 0000000..cd6dd3c --- /dev/null +++ b/src/Models/Ranking.php @@ -0,0 +1,327 @@ + 'array', + ]; + + /** + * The current element to reorder. + */ + protected $element = false; + + /** + * Set the ranks. + * + * @param array $ranks + * @return void + */ + public function setRanksAttribute(array $ranks): void + { + $this->attributes['ranks'] = json_encode($ranks); + } + + // RANKS METHODS + // ============================================================== + + /** + * Add an element in the rankings. + * Return the rank of the new element. + * + * @param $element + * @return int + */ + public function add($element): int + { + $rank = $this->rank($element); + + if ($rank === -1) { + $ranks = $this->ranks; + $ranks[] = $element; + $this->commit($ranks); + + return count($ranks); + } + + return $rank; + } + + /** + * Move the element first in the ranking. + * + * @return int + * @throws \Exception + */ + public function ahead(): int + { + if ($this->hasElement()) { + if ($this->element !== head($this->ranks)) { + $ranks = $this->ranks; + $ranks = array_diff($ranks, [$this->element]); + $this->commit(array_merge([$this->element], $ranks)); + } + + return 1; + } + } + + /** + * Save the ranks list in the ranking. + * + * @param array $ranks + * @return void + */ + protected function commit(array $ranks): void + { + $this->ranks = $ranks; + + $this->save(); + } + + /** + * Move the element one rank down. + * Return the new rank of the downgraded element. + * + * @return int + * @throws \Exception + */ + public function down(): int + { + if ($this->hasElement()) { + if ($this->element !== last($this->ranks)) { + $key = array_search($this->element, $this->ranks); + return $this->toggle($this->element, $this->ranks[$key + 1]); + } + + return count($this->ranks); + } + } + + /** + * Check that the element attribute is set. + * + * @return bool + * @throws \Exception + */ + protected function hasElement(): bool + { + if (isset($this->element)) { + return true; + } + + throw new \Exception('You must set the element with the move method before using this function. See documentation.'); + } + + /** + * Move the element last in the ranking. + * Return the rank of the last element. + * + * @return int + * @throws \Exception + */ + public function last(): int + { + if ($this->hasElement()) { + if ($this->element !== last($this->ranks)) { + $ranks = $this->ranks; + $ranks = array_diff($ranks, [$this->element]); + $this->commit(array_merge($ranks, [$this->element])); + } + + return count($this->ranks); + } + } + + /** + * Set the element id that is to reorder. + * + * @param $element + * @return self + * @throws \Exception + */ + public function move($element): self + { + if (in_array($element, $this->ranks)) { + $this->element = $element; + return $this; + } + + throw new \Exception('The element is not in the rankings.'); + } + + /** + * Move the element to a specific index. + * Return the new index of the element. + * + * @param int $index + * @return int + * @throws \Exception + */ + protected function moveTo(int $index): int + { + if ($this->hasElement()) { + $ranks = $this->ranks; + $ranks = array_diff($ranks, [$this->element]); + array_splice($ranks, $index, 0, $this->element); + $this->commit($ranks); + + return $index; + } + } + + /** + * Return the rank of the element in the ranking. + * + * @param $element + * @return int + */ + public function rank($element): int + { + if (in_array($element, $this->ranks)) { + return array_search($element, $this->ranks) + 1; + } + + return -1; + } + + /** + * Remove an item in the ranking. + * + * @param $element + * @return bool + */ + public function remove($element): bool + { + if (in_array($element, $this->ranks)) { + $ranks = $this->ranks; + $this->commit(array_values(array_diff($ranks, [$element]))); + } + + return true; + } + + /** + * Reverse the ranks in the ranking. + * + * @return void + */ + public function reverse(): void + { + $ranks = $this->ranks; + + $this->commit(array_values(collect($ranks)->reverse()->all())); + } + + /** + * Shuffle the ranks in the ranking. + * + * @return void + */ + public function shuffle(): void + { + $ranks = $this->ranks; + + $this->commit(collect($ranks)->shuffle()->all()); + } + + /** + * Move the element to a specific rank. + * Return the new rank of the element. + * + * @param int $rank + * @return int + * @throws \Exception + */ + public function toRank(int $rank): int + { + if ($rank < 1) { + $rank = 1; + } + + if ($rank > count($this->ranks)) { + $rank = count($this->ranks); + } + + $this->moveTo($rank - 1); + + return $rank; + } + + /** + * Toggle two elements in the ranking. + * Return the new rank of the first element. + * + * @param $first + * @param $last + * @return int + */ + public function toggle($firstElement, $lastElement): int + { + if ($firstElement !== $lastElement && in_array($firstElement, $this->ranks) && in_array($lastElement, $this->ranks)) { + $firstKey = array_search($firstElement, $this->ranks); + $lastKey = array_search($lastElement, $this->ranks); + $ranks = $this->ranks; + [$ranks[$firstKey], $ranks[$lastKey]] = [$ranks[$lastKey], $ranks[$firstKey]]; + $this->commit($ranks); + } + + return $this->rank($firstElement); + } + + /** + * Move the element to a specific index. + * Return the new index of the element. + * + * @param int $index + * @return int + * @throws \Exception + */ + public function toIndex(int $index): int + { + if ($index < 0) { + $index = 0; + } + + if ($index >= count($this->ranks)) { + $index = count($this->ranks) - 1; + } + + return $this->moveTo($index); + } + + /** + * Move the element one rank up. + * Return the rank of the upgraded element. + * + * @return int + * @throws \Exception + */ + public function up(): int + { + if ($this->hasElement()) { + if ($this->rank($this->element) > 1) { + $key = array_search($this->element, $this->ranks); + return $this->toggle($this->element, $this->ranks[$key - 1]); + } + + return 1; + } + } +} diff --git a/src/Scopes/InputScope.php b/src/Scopes/InputScope.php new file mode 100644 index 0000000..1341840 --- /dev/null +++ b/src/Scopes/InputScope.php @@ -0,0 +1,41 @@ +type = $type; + } + + /** + * Apply the scope to a given Eloquent query builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + */ + public function apply(Builder $builder, Model $model): void + { + $builder->where('type', '=', $this->type); + } +} diff --git a/src/Services/ValidatorService.php b/src/Services/ValidatorService.php new file mode 100644 index 0000000..0adf184 --- /dev/null +++ b/src/Services/ValidatorService.php @@ -0,0 +1,106 @@ + 'string|in:on,off,name,email,username,new-password,current-password,organization-title,organization,street-address,address-level1,address-level2,address-level3,address-level4,country,country-name,postal-code,cc-name,cc-given-name,cc-additional-name,cc-family-name,cc-number,cc-exp,cc-exp-month,cc-exp-year,cc-csc,cc-type,transaction-currency,transaction-amount,language,bday,bday-day,bday-month,bday-year,sex,tel,tel-extension,email,impp,url,photo', + 'html_properties.cols' => 'integer|min:0', + 'html_properties.enctype' => 'string|in:application/x-www-form-urlencoded,multipart/form-data,text/plain', + 'html_properties.height' => 'integer|min:0', + 'html_properties.rows' => 'integer|min:0', + 'html_properties.width' => 'integer|min:0', + ]; + + /** + * Validate the model attributes + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Validation\Validator + */ + public function validate(Model $model): \Illuminate\Validation\Validator + { + $this->model = $model; + + $attributes = $this->getAttributes(); + + $rules = $this->getValidationRules($attributes); + + return Validator::make($attributes->all(), $rules->all()); + } + + /** + * Get a formatted array of the model attributes. + * + * @return \Illuminate\Support\Collection + */ + protected function getAttributes(): Collection + { + $attributes = collect($this->model->getAttributes()); + + $attributes['html_properties'] = $this->model->html_properties ?? []; + + return $attributes; + } + + /** + * Get the validation rules of the attributes. + * + * @param \Illuminate\Support\Collection $attributes + * @return \Illuminate\Support\Collection + */ + protected function getBasicRules(Collection $attributes): Collection + { + return collect($this->rules)->intersectByKeys( + array_dot($attributes->all()) + ); + } + + /** + * Get the validation rules of the attributes. + * + * @param \Illuminate\Support\Collection $attributes + * @return \Illuminate\Support\Collection + */ + protected function getValidationRules(Collection $attributes): Collection + { + $rules = $this->getBasicRules($attributes); + + return $this->mergeAdditionalRules($rules); + } + + /** + * Append the additional rules to the default rules for each attribute. + * + * @param \Illuminate\Support\Collection $rules + * @return \Illuminate\Support\Collection + */ + protected function mergeAdditionalRules(Collection $rules): Collection + { + $additionalRules = $this->model->additionalRules ?? []; + + foreach ($additionalRules as $attribute => $additionalRule) { + $initialRules = $rules[$attribute] ?? ''; + $rules[$attribute] = sprintf('%s|%s', $initialRules, $additionalRule); + } + + return $rules; + } +} diff --git a/src/Traits/HasInputs.php b/src/Traits/HasInputs.php new file mode 100644 index 0000000..dacbff2 --- /dev/null +++ b/src/Traits/HasInputs.php @@ -0,0 +1,134 @@ +getInputPath($type); + + $input = new $inputPath; + + $input->assign('properties', $properties); + + $input->assign('rules', $rules); + + $this->inputsBuilder($type)->save($input); + + return $input; + } + + /** + * Add multiple input to the parent model. + * + * @param string $type + * @param array $values + * @return void + * @throws \Exception + */ + public function addMultiple(string $type, array $values): void + { + foreach ($values as $input) { + $this->add(...[$type, $input]); + } + } + + /** + * Get the model inputs. + * + * @return \Illuminate\Support\Collection + * @throws \Exception + */ + protected function getAllInputs(): Collection + { + $inputs = collect([]); + $rawInputs = DB::table('inputs')->get()->groupBy('type')->toArray(); + + foreach (array_keys($rawInputs) as $type) { + $subset = $this->inputsBuilder($type)->get(); + $inputs = $inputs->merge($subset); + } + + return $inputs; + } + + + /** + * Get the class input full path. + * + * @param string $input + * @return string + * @throws \Exception + */ + protected function getInputPath(string $input): string + { + $className = 'Chess\\FormMaker\\Models\\Form\\Inputs\\' . ucfirst($input); + + if (class_exists($className)) { + return $className; + } + + throw new \Exception('This input is not supported'); + } + + /** + * Get the model inputs with their rank. + * + * @param string $type + * @return \Illuminate\Support\Collection + * @throws \Exception + */ + public function inputs(string $type = ''): Collection + { + if ($type) { + $inputs = $this->inputsBuilder($type)->get(); + } else { + $inputs = $this->getAllInputs(); + } + + return $inputs->map(function ($input) { + $input->rank = $this->ranking->rank($input->id); + return $input; + }); + } + + /** + * Get the model inputs query builder. + * + * @param string $type + * @return \Illuminate\Database\Eloquent\Relations\MorphMany + * @throws \Exception + */ + protected function inputsBuilder(string $type): MorphMany + { + return $this->morphMany($this->getInputPath($type), 'inputable'); + } +} diff --git a/src/Traits/HasRanking.php b/src/Traits/HasRanking.php new file mode 100644 index 0000000..1116e43 --- /dev/null +++ b/src/Traits/HasRanking.php @@ -0,0 +1,39 @@ +load('ranking'); + }); + + static::deleted(function (Model $model) { + event(new DeleteRanking($model)); + }); + } + + /** + * Get the model ranking. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphOne + */ + public function ranking(): MorphOne + { + return $this->morphOne('Chess\FormMaker\Models\Ranking', 'rankable'); + } +} diff --git a/src/Traits/InputRules.php b/src/Traits/InputRules.php new file mode 100644 index 0000000..e5687a8 --- /dev/null +++ b/src/Traits/InputRules.php @@ -0,0 +1,994 @@ +rules = ['accepted' => $accepted]; + + return $this; + } + + /** + * The field under validation must have a valid A or AAAA record according + * to the dns_get_record PHP function. + * + * @param string $activeUrl + * @return self + */ + public function ruleActiveUrl(?string $activeUrl = 'active_url'): self + { + $this->rules = ['active_url' => $activeUrl]; + + return $this; + } + + /** + * The field under validation must be a value after a given date. + * The dates will be passed into the strtotime PHP function. + * + * @param string $date + * @return self + */ + public function ruleAfter(?string $date): self + { + if (is_null($date)) { + $this->rules = ['after' => null]; + } else { + $this->rules = ['after' => sprintf('after:%s', $date)]; + } + + return $this; + } + + /** + * The field under validation must be a value after or equal to the given date. + * For more information, see the after rule. + * + * @param string $date + * @return self + */ + public function ruleAfterOrEqual(?string $date): self + { + if (is_null($date)) { + $this->rules = ['after_or_equal' => null]; + } else { + $this->rules = ['after_or_equal' => sprintf('after_or_equal:%s', $date)]; + } + + return $this; + } + + /** + * The field under validation must be entirely alphabetic characters. + * + * @param string $alpha + * @return self + */ + public function ruleAlpha(?string $alpha = 'alpha'): self + { + $this->rules = ['alpha' => $alpha]; + + return $this; + } + + /** + * The field under validation may have alpha-numeric characters, + * as well as dashes and underscores. + * + * @param string $alphaDash + * @return self + */ + public function ruleAlphaDash(?string $alphaDash = 'alpha_dash'): self + { + $this->rules = ['alpha_dash' => $alphaDash]; + + return $this; + } + + /** + * The field under validation must be entirely alpha-numeric characters. + * + * @param string $alphaNum + * @return self + */ + public function ruleAlphaNum(?string $alphaNum = 'alpha_num'): self + { + $this->rules = ['alpha_num' => $alphaNum]; + + return $this; + } + + /** + * The field under validation must be a PHP array. + * + * @param string $array + * @return self + */ + public function ruleArray(?string $array = 'array'): self + { + $this->rules = ['array' => $array]; + + return $this; + } + + /** + * Stop running validation rules after the first validation failure. + * + * @param string $bail + * @return self + */ + public function ruleBail(?string $bail = 'bail'): self + { + $this->rules = ['bail' => $bail]; + + return $this; + } + + /** + * The field under validation must be a value preceding the given date. + * The dates will be passed into the PHP strtotime function. + * In addition, like the after rule, the name of another field under + * validation may be supplied as the value of date. + * + * @param string $date + * @return self + */ + public function ruleBefore(?string $date): self + { + if (is_null($date)) { + $this->rules = ['before' => null]; + } else { + $this->rules = ['before' => sprintf('before:%s', $date)]; + } + + return $this; + } + + /** + * The field under validation must be a value preceding or equal to the given date. + * The dates will be passed into the PHP strtotime function. + * In addition, like the after rule, the name of another field under + * validation may be supplied as the value of date. + * + * @param string $date + * @return self + */ + public function ruleBeforeOrEqual(?string $date): self + { + if (is_null($date)) { + $this->rules = ['before_or_equal' => null]; + } else { + $this->rules = ['before_or_equal' => sprintf('before_or_equal:%s', $date)]; + } + + return $this; + } + + /** + * The field under validation must have a size between the given min and max. + * Strings, numerics, arrays, and files are evaluated in the same fashion as the size rule. + * + * @param int|null ...$interval + * @return self + */ + public function ruleBetween(?int ...$interval): self + { + $interval = $this->removeNullValues($interval); + + if (count($interval) === 0) { + $this->rules = ['between' => null]; + } else { + $this->rules = ['between' => sprintf('between:%s', implode(',', $interval))]; + } + + return $this; + } + + /** + * The field under validation must be able to be cast as a boolean. + * + * @param string $boolean + * @return self + */ + public function ruleBoolean(?string $boolean = 'boolean'): self + { + $this->rules = ['boolean' => $boolean]; + + return $this; + } + + /** + * The field under validation must have a matching field of foo_confirmation. + * For example, if the field under validation is password, + * a matching password_confirmation field must be present in the input. + * + * @param string $confirmed + * @return self + */ + public function ruleConfirmed(?string $confirmed = 'confirmed'): self + { + $this->rules = ['confirmed' => $confirmed]; + + return $this; + } + + /** + * The field under validation must be a valid date according to the strtotime PHP function. + * + * @param string $date + * @return self + */ + public function ruleDate(?string $date = 'date'): self + { + $this->rules = ['date' => $date]; + + return $this; + } + + /** + * The field under validation must be equal to the given date. + * The dates will be passed into the PHP strtotime function. + * + * @param string $date + * @return self + */ + public function ruleDateEquals(?string $date): self + { + if (is_null($date)) { + $this->rules = ['date_equals' => null]; + } else { + $this->rules = ['date_equals' => sprintf('date_equals:%s', $date)]; + } + + return $this; + } + + /** + * The field under validation must match the given format. + * You should use either date or date_format when validating a field, not both. + * + * @param string $format + * @return self + */ + public function ruleDateFormat(?string $format): self + { + if (is_null($format)) { + $this->rules = ['date_format' => null]; + } else { + $this->rules = ['date_format' => sprintf('date_format:%s', $format)]; + } + + return $this; + } + + /** + * The field under validation must have a different value than field. + * + * @param string $field + * @return self + */ + public function ruleDifferent(?string $field): self + { + if (is_null($field)) { + $this->rules = ['different' => null]; + } else { + $this->rules = ['different' => sprintf('different:%s', $field)]; + } + + return $this; + } + + /** + * The field under validation must be numeric and must have an exact length of value. + * + * @param int $value + * @return self + */ + public function ruleDigits(?int $value): self + { + if (is_null($value)) { + $this->rules = ['digits' => null]; + } else { + $this->rules = ['digits' => sprintf('digits:%d', $value)]; + } + + return $this; + } + + /** + * The field under validation must have a length between the given min and max. + * + * @param int $interval + * @return self + */ + public function ruleDigitsBetween(?int ...$interval): self + { + $interval = $this->removeNullValues($interval); + + if (count($interval) === 0) { + $this->rules = ['digits_between' => null]; + } else { + $this->rules = ['digits_between' => sprintf('digits_between:%s', implode(',', $interval))]; + } + + return $this; + } + + /** + * The file under validation must be an image meeting the dimension + * constraints as specified by the rule's parameters. + * + * @param null|string ...$dimensions + * @return self + */ + public function ruleDimensions(?string ...$dimensions): self + { + $dimensions = $this->removeNullValues($dimensions); + + if (count($dimensions) === 0) { + $this->rules = ['dimensions' => null]; + } else { + $this->rules = ['dimensions' => sprintf( + 'dimensions:%s', implode(',', $dimensions) + )]; + } + + return $this; + } + + /** + * When working with arrays, the field under validation must not have any duplicate values. + * + * @param string $distinct + * @return self + */ + public function ruleDistinct(?string $distinct = 'distinct'): self + { + $this->rules = ['distinct' => $distinct]; + + return $this; + } + + /** + * The field under validation must be formatted as an e-mail address. + * + * @param string $email + * @return self + */ + public function ruleEmail(?string $email = 'email'): self + { + $this->rules = ['email' => $email]; + + return $this; + } + + /** + * The field under validation must exist on a given database table. + * + * @param null|string ...$values + * @return self + */ + public function ruleExists(?string ...$values): self + { + $values = $this->removeNullValues($values); + + if (count($values) === 0) { + $this->rules = ['exists' => null]; + } else { + $this->rules = ['exists' => sprintf('exists:%s', implode(',', $values))]; + } + + return $this; + } + + /** + * The field under validation must be a successfully uploaded file. + * + * @param string $file + * @return self + */ + public function ruleFile(?string $file = 'file'): self + { + $this->rules = ['file' => $file]; + + return $this; + } + + /** + * The field under validation must not be empty when it is present. + * + * @param string $filled + * @return self + */ + public function ruleFilled(?string $filled = 'filled'): self + { + $this->rules = ['filled' => $filled]; + + return $this; + } + + /** + * The field under validation must be greater than the given field. + * The two fields must be of the same type. Strings, numerics, arrays, + * and files are evaluated using the same conventions as the size rule. + * + * @param string $field + * @return self + */ + public function ruleGreaterThan(?string $field): self + { + if (is_null($field)) { + $this->rules = ['gt' => null]; + } else { + $this->rules = ['gt' => sprintf('gt:%s', $field)]; + } + + return $this; + } + + /** + * The field under validation must be greater than or equal to the given field. + * The two fields must be of the same type. Strings, numerics, arrays, + * and files are evaluated using the same conventions as the size rule. + * + * @param string $field + * @return self + */ + public function ruleGreaterThanOrEqual(?string $field): self + { + if (is_null($field)) { + $this->rules = ['gte' => null]; + } else { + $this->rules = ['gte' => sprintf('gte:%s', $field)]; + } + + return $this; + } + + /** + * The file under validation must be an image (jpeg, png, bmp, gif, or svg). + * + * @param string $image + * @return self + */ + public function ruleImage(?string $image = 'image'): self + { + $this->rules = ['image' => $image]; + + return $this; + } + + /** + * The field under validation must be included in the given list of values. + * + * @param null|string ...$list + * @return self + */ + public function ruleIn(?string ...$list): self + { + $list = $this->removeNullValues($list); + + if (count($list) === 0) { + $this->rules = ['in' => null]; + } else { + $this->rules = ['in' => sprintf('in:%s', implode(',', $list))]; + } + + return $this; + } + + /** + * The field under validation must exist in anotherfield's values. + * + * @param string $anotherfield + * @return self + */ + public function ruleInArray(?string $anotherfield): self + { + if (is_null($anotherfield)) { + $this->rules = ['in_array' => null]; + } else { + $this->rules = ['in_array' => sprintf('in_array:%s', $anotherfield)]; + } + + return $this; + } + + /** + * The field under validation must be an integer. + * + * @param string $integer + * @return self + */ + public function ruleInteger(?string $integer = 'integer'): self + { + $this->rules = ['integer' => $integer]; + + return $this; + } + + /** + * The field under validation must be an integer. + * + * @param string $ip + * @return self + */ + public function ruleIp(?string $ip = 'ip'): self + { + $this->rules = ['ip' => $ip]; + + return $this; + } + + /** + * The field under validation must be an IPv4 address. + * + * @param string $ipv4 + * @return self + */ + public function ruleIpv4(?string $ipv4 = 'ipv4'): self + { + $this->rules = ['ipv4' => $ipv4]; + + return $this; + } + + /** + * The field under validation must be an IPv6 address. + * + * @param string $ipv6 + * @return self + */ + public function ruleIpv6(?string $ipv6 = 'ipv6'): self + { + $this->rules = ['ipv6' => $ipv6]; + + return $this; + } + + /** + * The field under validation must be a valid JSON string. + * + * @param string $json + * @return self + */ + public function ruleJson(?string $json = 'json'): self + { + $this->rules = ['json' => $json]; + + return $this; + } + + /** + * The field under validation must be less than the given field. + * The two fields must be of the same type. Strings, numerics, arrays, and + * files are evaluated using the same conventions as the size rule. + * + * @param string $field + * @return self + */ + public function ruleLessThan(?string $field): self + { + if (is_null($field)) { + $this->rules = ['lt' => null]; + } else { + $this->rules = ['lt' => sprintf('lt:%s', $field)]; + } + + return $this; + } + + /** + * The field under validation must be less than or equal to the given field. + * The two fields must be of the same type. Strings, numerics, arrays, + * and files are evaluated using the same conventions as the size rule. + * + * @param string $field + * @return self + */ + public function ruleLessThanOrEqual(?string $field): self + { + if (is_null($field)) { + $this->rules = ['lte' => null]; + } else { + $this->rules = ['lte' => sprintf('lte:%s', $field)]; + } + + return $this; + } + + /** + * The field under validation must be less than or equal to a maximum value. + * Strings, numerics, arrays, and files are evaluated in the same fashion + * as the size rule. + * + * @param int $max + * @return self + */ + public function ruleMax(?int $max): self + { + if (is_null($max)) { + $this->rules = ['max' => null]; + } else { + $this->rules = ['max' => sprintf('max:%d', $max)]; + } + + return $this; + } + + /** + * The file under validation must match one of the given MIME types + * + * @param null|string ...$types + * @return self + */ + public function ruleMimetypes(?string ...$types): self + { + $types = $this->removeNullValues($types); + + if (count($types) === 0) { + $this->rules = ['mimetypes' => null]; + } else { + $this->rules = ['mimetypes' => sprintf( + 'mimetypes:%s', implode(',', $types) + )]; + } + + return $this; + } + + /** + * The file under validation must match one of the given MIME types + * + * @param null|string ...$mimes + * @return self + */ + public function ruleMimes(?string ...$mimes): self + { + $mimes = $this->removeNullValues($mimes); + + if (count($mimes) === 0) { + $this->rules = ['mimes' => null]; + } else { + $this->rules = ['mimes' => sprintf( + 'mimes:%s', implode(',', $mimes) + )]; + } + + return $this; + } + + /** + * The field under validation must have a minimum value. + * Strings, numerics, arrays, and files are evaluated in the same fashion + * as the size rule. + * + * @param int $min + * @return self + */ + public function ruleMin(?int $min): self + { + if (is_null($min)) { + $this->rules = ['min' => null]; + } else { + $this->rules = ['min' => sprintf('min:%d', $min)]; + } + + return $this; + } + + /** + * The field under validation must not be included in the given list of values. + * + * @param null|string ...$list + * @return self + */ + public function ruleNotIn(?string ...$list): self + { + $list = $this->removeNullValues($list); + + if (count($list) === 0) { + $this->rules = ['not_in' => null]; + } else { + $this->rules = ['not_in' => sprintf( + 'not_in:%s', implode(',', $list) + )]; + } + + return $this; + } + + /** + * The field under validation may be null. + * This is particularly useful when validating primitive such as strings and + * integers that can contain null values. + * + * @param string $nullable + * @return self + */ + public function ruleNullable(?string $nullable = 'nullable'): self + { + $this->rules = ['nullable' => $nullable]; + + return $this; + } + + /** + * The field under validation must be numeric. + * + * @param string $numeric + * @return self + */ + public function ruleNumeric(?string $numeric = 'numeric'): self + { + $this->rules = ['numeric' => $numeric]; + + return $this; + } + + /** + * The field under validation must be present in the input data but can be empty. + * + * @param string $present + * @return self + */ + public function rulePresent(?string $present = 'present'): self + { + $this->rules = ['present' => $present]; + + return $this; + } + + /** + * The field under validation must be present in the input data and not empty. + * + * @param string $required + * @return self + */ + public function ruleRequired(?string $required = 'required'): self + { + $this->rules = ['required' => $required]; + + return $this; + } + + /** + * The field under validation must be present and not empty if + * the anotherfield field is equal to any value. + * + * @param null|string ...$field + * @return self + */ + public function ruleRequiredIf(?string ...$field): self + { + $field = $this->removeNullValues($field); + + if (count($field) === 0) { + $this->rules = ['required_if' => null]; + } else { + $this->rules = ['required_if' => sprintf( + 'required_if:%s,%s', implode(',', $field) + )]; + } + + return $this; + } + + /** + * The field under validation must be present and not empty unless + * the anotherfield field is equal to any value. + * + * @param null|string ...$field + * @return self + */ + public function ruleRequiredUnless(?string ...$field): self + { + $field = $this->removeNullValues($field); + + if (count($field) === 0) { + $this->rules = ['required_unless' => null]; + } else { + $this->rules = ['required_unless' => sprintf( + 'required_unless:%s', implode(',', $field) + )]; + } + + return $this; + } + + /** + * The field under validation must be present and not empty only if any + * of the other specified fields are present. + * + * @param null|string ...$fields + * @return self + */ + public function ruleRequiredWith(?string ...$fields): self + { + $fields = $this->removeNullValues($fields); + + if (count($fields) === 0) { + $this->rules = ['required_with' => null]; + } else { + $this->rules = ['required_with' => sprintf( + 'required_with:%s', implode(',', $fields) + )]; + } + + return $this; + } + + /** + * The field under validation must be present and not empty only if all + * of the other specified fields are present. + * + * @param null|string ...$fields + * @return self + */ + public function ruleRequiredWithAll(?string ...$fields): self + { + $fields = $this->removeNullValues($fields); + + if (count($fields) === 0) { + $this->rules = ['required_with_all' => null]; + } else { + $this->rules = ['required_with_all' => sprintf( + 'required_with_all:%s', implode(',', $fields) + )]; + } + + return $this; + } + + /** + * The field under validation must be present and not empty only when any + * of the other specified fields are not present. + * + * @param null|string ...$fields + * @return self + */ + public function ruleRequiredWithout(?string ...$fields): self + { + $fields = $this->removeNullValues($fields); + + if (count($fields) === 0) { + $this->rules = ['required_without' => null]; + } else { + $this->rules = ['required_without' => sprintf( + 'required_without:%s', implode(',', $fields) + )]; + } + + return $this; + } + + /** + * The field under validation must be present and not empty only when all + * of the other specified fields are not present. + * + * @param null|string ...$fields + * @return self + */ + public function ruleRequiredWithoutAll(?string ...$fields): self + { + $fields = $this->removeNullValues($fields); + + if (count($fields) === 0) { + $this->rules = ['required_without_all' => null]; + } else { + $this->rules = ['required_without_all' => sprintf( + 'required_without_all:%s', implode(',', $fields) + )]; + } + + return $this; + } + + /** + * The given field must match the field under validation. + * + * @param string $field + * @return self + */ + public function ruleSame(?string $field): self + { + if (is_null($field)) { + $this->rules = ['same' => null]; + } else { + $this->rules = ['same' => sprintf('same:%s', $field)]; + } + + return $this; + } + + /** + * The field under validation must have a size matching the given value. + * For string data, value corresponds to the number of characters. + * For numeric data, value corresponds to a given integer value. + * For an array, size corresponds to the count of the array. + * For files, size corresponds to the file size in kilobytes. + * + * @param int $value + * @return self + */ + public function ruleSize(?int $value): self + { + if (is_null($value)) { + $this->rules = ['size' => null]; + } else { + $this->rules = ['size' => sprintf('size:%d', $value)]; + } + + return $this; + } + + /** + * The field under validation must be a string. If you would like to allow + * the field to also be null, you should assign the nullable rule to the field. + * + * @param string $string + * @return self + */ + public function ruleString(?string $string = 'string'): self + { + $this->rules = ['string' => $string]; + + return $this; + } + + /** + * The field under validation must be a valid timezone identifier according + * to the timezone_identifiers_list PHP function. + * + * @param string $timezone + * @return self + */ + public function ruleTimezone(?string $timezone = 'timezone'): self + { + $this->rules = ['timezone' => $timezone]; + + return $this; + } + + /** + * The field under validation must be unique in a given database table. + * + * @param null|string ...$values + * @return self + */ + public function ruleUnique(?string ...$values): self + { + $values = $this->removeNullValues($values); + + if (count($values) === 0) { + $this->rules = ['unique' => null]; + } else { + $this->rules = ['unique' => sprintf('unique:%s', implode(',', $values))]; + } + + return $this; + } + + /** + * The field under validation must be a valid URL. + * + * @param string $url + * @return self + */ + public function ruleUrl(?string $url = 'url'): self + { + $this->rules = ['url' => $url]; + + return $this; + } +} diff --git a/src/Traits/Properties/FormProperties.php b/src/Traits/Properties/FormProperties.php new file mode 100644 index 0000000..e3e484d --- /dev/null +++ b/src/Traits/Properties/FormProperties.php @@ -0,0 +1,61 @@ +html_properties = ['charset' => $charset]; + + return $this; + } + + /** + * Specifies the encoding of the submitted data. + * default: is url-encoded + * + * @param string $enctype + * @return self + */ + public function propertyEnctype(?string $enctype): self + { + $this->html_properties = ['enctype' => $enctype]; + + return $this; + } + + /** + * Specifies that the browser should not validate the form. + * + * @param string $novalidate + * @return self + */ + public function propertyNovalidate(?string $novalidate = 'novalidate'): self + { + $this->html_properties = ['novalidate' => $novalidate]; + + return $this; + } + + /** + * Specifies the target of the address in the action attribute. + * This will make the form result open in a new browser tab. + * + * @param string $blank + * @return self + */ + public function propertyResultsInNewWindow(?string $blank = '_blank'): self + { + $this->html_properties = ['target' => $blank]; + + return $this; + } +} diff --git a/src/Traits/Properties/GlobalProperties.php b/src/Traits/Properties/GlobalProperties.php new file mode 100644 index 0000000..37907de --- /dev/null +++ b/src/Traits/Properties/GlobalProperties.php @@ -0,0 +1,59 @@ +html_properties = ['class' => $class]; + + return $this; + } + + /** + * Specifies data attribute for the html element. + * + * @param string $data + * @param mixed $value + * @return self + */ + public function propertyData(string $data, $value = null): self + { + $this->html_properties = [$data => $value]; + + return $this; + } + + /** + * Specifies id attribute for the html element. + * + * @param mixed $id + * @return self + */ + public function propertyId($id = null): self + { + $this->html_properties = ['id' => $id]; + + return $this; + } + + /** + * Specifies a name used to identify the form or the input. + * + * @param string $name + * @return self + */ + public function propertyName(?string $name): self + { + $this->html_properties = ['name' => $name]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasAutocomplete.php b/src/Traits/Properties/HasAutocomplete.php new file mode 100644 index 0000000..baf740f --- /dev/null +++ b/src/Traits/Properties/HasAutocomplete.php @@ -0,0 +1,20 @@ +html_properties = ['autocomplete' => $autocomplete]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasAutofocus.php b/src/Traits/Properties/HasAutofocus.php new file mode 100644 index 0000000..0a96d98 --- /dev/null +++ b/src/Traits/Properties/HasAutofocus.php @@ -0,0 +1,19 @@ +html_properties = ['autofocus' => $autofocus]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasChecked.php b/src/Traits/Properties/HasChecked.php new file mode 100644 index 0000000..84728db --- /dev/null +++ b/src/Traits/Properties/HasChecked.php @@ -0,0 +1,19 @@ +html_properties = ['checked' => $checked]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasMinMax.php b/src/Traits/Properties/HasMinMax.php new file mode 100644 index 0000000..705be51 --- /dev/null +++ b/src/Traits/Properties/HasMinMax.php @@ -0,0 +1,47 @@ +html_properties = ['max' => $max]; + + return $this; + } + + /** + * Specifies the minimum value for an input field. + * + * @param mixed $min + * @return self + */ + public function propertyMin($min = null): self + { + $this->html_properties = ['min' => $min]; + + return $this; + } + + /** + * Specifies the legal number intervals for an input field. + * The control accepts only values at multiples of the step value + * greater than the minimum + * + * @param $step + * @return self + */ + public function htmlStep($step = null): self + { + $this->html_properties = ['step' => $step]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasMinMaxLength.php b/src/Traits/Properties/HasMinMaxLength.php new file mode 100644 index 0000000..25b9bb9 --- /dev/null +++ b/src/Traits/Properties/HasMinMaxLength.php @@ -0,0 +1,32 @@ +html_properties = ['maxlength' => $maxlength]; + + return $this; + } + + /** + * Specifies the minimum allowed length for the input field. + * + * @param int $minlength + * @return self + */ + public function propertyMinlength(?int $minlength): self + { + $this->html_properties = ['minlength' => $minlength]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasMultiple.php b/src/Traits/Properties/HasMultiple.php new file mode 100644 index 0000000..c807596 --- /dev/null +++ b/src/Traits/Properties/HasMultiple.php @@ -0,0 +1,19 @@ +html_properties = ['multiple' => $multiple]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasPattern.php b/src/Traits/Properties/HasPattern.php new file mode 100644 index 0000000..e52ef7d --- /dev/null +++ b/src/Traits/Properties/HasPattern.php @@ -0,0 +1,19 @@ +html_properties = ['pattern' => $pattern]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasPlaceholder.php b/src/Traits/Properties/HasPlaceholder.php new file mode 100644 index 0000000..96e4678 --- /dev/null +++ b/src/Traits/Properties/HasPlaceholder.php @@ -0,0 +1,20 @@ +html_properties = ['placeholder' => $placeholder]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasReadonly.php b/src/Traits/Properties/HasReadonly.php new file mode 100644 index 0000000..2ca450e --- /dev/null +++ b/src/Traits/Properties/HasReadonly.php @@ -0,0 +1,19 @@ +html_properties = ['readonly' => $readonly]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasRequired.php b/src/Traits/Properties/HasRequired.php new file mode 100644 index 0000000..1c5538e --- /dev/null +++ b/src/Traits/Properties/HasRequired.php @@ -0,0 +1,19 @@ +html_properties = ['required' => $required]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasSize.php b/src/Traits/Properties/HasSize.php new file mode 100644 index 0000000..d833e01 --- /dev/null +++ b/src/Traits/Properties/HasSize.php @@ -0,0 +1,20 @@ +html_properties = ['size' => $size]; + + return $this; + } +} diff --git a/src/Traits/Properties/HasSpellcheck.php b/src/Traits/Properties/HasSpellcheck.php new file mode 100644 index 0000000..dbff56f --- /dev/null +++ b/src/Traits/Properties/HasSpellcheck.php @@ -0,0 +1,20 @@ +html_properties = ['spellcheck' => $spellcheck]; + + return $this; + } +} diff --git a/src/Traits/Properties/InputProperties.php b/src/Traits/Properties/InputProperties.php new file mode 100644 index 0000000..06e81f8 --- /dev/null +++ b/src/Traits/Properties/InputProperties.php @@ -0,0 +1,45 @@ +html_properties = ['disabled' => $disabled]; + + return $this; + } + + /** + * Add a description to help the user. + * + * @param string $title + * @return self + */ + public function propertyTitle(?string $title): self + { + $this->html_properties = ['title' => $title]; + + return $this; + } + + /** + * Specifies the value property for an input field. + * + * @param string $value + * @return self + */ + public function propertyValue(?string $value): self + { + $this->html_properties = ['value' => $value]; + + return $this; + } +}