├── src ├── Traitify.php ├── Contracts │ ├── Builder.php │ ├── Execute.php │ ├── Processor.php │ ├── Menu.php │ ├── Api.php │ ├── Enum.php │ └── ValueGenerator.php ├── Facades │ └── Traitify.php ├── Commands │ └── TraitifyCommand.php ├── Concerns │ ├── InteractsWithDetails.php │ ├── InteractsWithMeta.php │ ├── InteractsWithUser.php │ ├── InteractsWithSearchable.php │ ├── InteractsWithResourceRoute.php │ ├── InteractsWithApi.php │ ├── InteractsWithEnum.php │ ├── InteractsWithToken.php │ ├── InteractsWithSqlViewMigration.php │ ├── InteractsWithUuid.php │ ├── InteractsWithSlug.php │ ├── HasGeneratorResolver.php │ └── InteractsWithTags.php ├── TraitifyServiceProvider.php └── Generators │ ├── AbstractValueGenerator.php │ ├── TokenGenerator.php │ ├── UuidGenerator.php │ └── SlugGenerator.php ├── .claude └── settings.local.json ├── LICENSE.md ├── config └── traitify.php ├── composer.json ├── README.md └── CHANGELOG.md /src/Traitify.php: -------------------------------------------------------------------------------- 1 | comment('All done'); 16 | 17 | return self::SUCCESS; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithDetails.php: -------------------------------------------------------------------------------- 1 | with_details) 12 | ? $this->with_details 13 | : []; 14 | } 15 | 16 | public function scopeWithDetails(Builder $query): Builder 17 | { 18 | return $query->with($this->getDetails()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Contracts/Api.php: -------------------------------------------------------------------------------- 1 | name('traitify') 19 | ->hasConfigFile(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithMeta.php: -------------------------------------------------------------------------------- 1 | getTable(), 'meta') && is_null($model->meta)) { 13 | // Set the default 'meta' value 14 | $model->meta = $model->defaultMeta(); 15 | } 16 | }); 17 | } 18 | 19 | public function defaultMeta() 20 | { 21 | return property_exists($this, 'default_meta') 22 | ? $this->default_meta 23 | : []; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Contracts/Enum.php: -------------------------------------------------------------------------------- 1 | getTable(), $model->getUserIdColumnName()) && is_null($model->user_id) && Auth::user()) { 14 | $model->{$model->getUserIdColumnName()} = Auth::user()->id; 15 | } 16 | }); 17 | } 18 | 19 | /** 20 | * Get User's ID Column Name. 21 | */ 22 | public function getUserIdColumnName(): string 23 | { 24 | return isset($this->user_id_column) ? $this->user_id_column : 'user_id'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithSearchable.php: -------------------------------------------------------------------------------- 1 | orWhereRaw('LOWER('.$field.') LIKE ?', ['%'.$keyword.'%']); 27 | } 28 | 29 | return $query; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithResourceRoute.php: -------------------------------------------------------------------------------- 1 | getUrlRouteBaseName().'.index') 11 | : route($this->getUrlRouteBaseName().'.'.$type, $this); 12 | } 13 | 14 | public function getUrlRouteBaseName() 15 | { 16 | return 17 | $this->getUrlRoutePrefix(). 18 | str(get_called_class())->classBasename()->kebab()->plural()->toString(); 19 | } 20 | 21 | public function getUrlRoutePrefix() 22 | { 23 | return 24 | property_exists($this, 'url_route_prefix') && ! empty($this->url_route_prefix) 25 | ? $this->url_route_prefix.'.' 26 | : ''; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithApi.php: -------------------------------------------------------------------------------- 1 | $this->getData($request), 17 | 'message' => $this->getMessage(), 18 | 'code' => $this->getCode(), 19 | ]; 20 | } 21 | 22 | public function getData(Request $request): JsonResource|ResourceCollection|array 23 | { 24 | return self::toArray($request); 25 | } 26 | 27 | public function getMessage(): string 28 | { 29 | return ''; 30 | } 31 | 32 | public function getCode(): int 33 | { 34 | return $this->code; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithEnum.php: -------------------------------------------------------------------------------- 1 | $case->value, self::cases() 14 | ); 15 | } 16 | 17 | /** 18 | * Get an array of enum label. 19 | */ 20 | public static function labels(): array 21 | { 22 | return array_map( 23 | fn ($case) => $case->label(), self::cases() 24 | ); 25 | } 26 | 27 | /** 28 | * Generate an array of options with value, label, and description for select inputs. 29 | */ 30 | public static function options(): array 31 | { 32 | return array_map(fn ($case) => [ 33 | 'value' => $case->value, 34 | 'label' => $case->label(), 35 | 'description' => $case->description(), 36 | ], self::cases()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Contracts/ValueGenerator.php: -------------------------------------------------------------------------------- 1 | $context Additional context (model, column, etc.) 11 | * @return mixed The generated value 12 | */ 13 | public function generate(array $context = []): mixed; 14 | 15 | /** 16 | * Validate a generated or existing value. 17 | * 18 | * @param mixed $value The value to validate 19 | * @param array $context Additional context 20 | * @return bool True if valid, false otherwise 21 | */ 22 | public function validate(mixed $value, array $context = []): bool; 23 | 24 | /** 25 | * Get the configuration for this generator. 26 | * 27 | * @return array 28 | */ 29 | public function getConfig(): array; 30 | 31 | /** 32 | * Set the configuration for this generator. 33 | * 34 | * @param array $config 35 | */ 36 | public function setConfig(array $config): self; 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Cleanique Coders 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Generators/AbstractValueGenerator.php: -------------------------------------------------------------------------------- 1 | config = array_merge($this->getDefaultConfig(), $config); 14 | } 15 | 16 | /** 17 | * Get default configuration for this generator. 18 | * 19 | * @return array 20 | */ 21 | abstract protected function getDefaultConfig(): array; 22 | 23 | public function getConfig(): array 24 | { 25 | return $this->config; 26 | } 27 | 28 | public function setConfig(array $config): self 29 | { 30 | $this->config = array_merge($this->config, $config); 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Get a configuration value with dot notation support. 37 | */ 38 | protected function getConfigValue(string $key, mixed $default = null): mixed 39 | { 40 | return data_get($this->config, $key, $default); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithToken.php: -------------------------------------------------------------------------------- 1 | getTable(), $model->getTokenColumn()) && is_null($model->{$model->getTokenColumn()})) { 17 | $generator = $model->resolveGenerator( 18 | 'token', 19 | 'tokenGenerator', 20 | 'tokenGeneratorConfig' 21 | ); 22 | 23 | $model->{$model->getTokenColumn()} = $generator->generate([ 24 | 'model' => $model, 25 | 'column' => $model->getTokenColumn(), 26 | ]); 27 | } 28 | }); 29 | } 30 | 31 | /** 32 | * Get Token Column Name. 33 | */ 34 | public function getTokenColumn(): string 35 | { 36 | return $this->token_column ?? 'token'; 37 | } 38 | 39 | /** 40 | * Scope a query based on token field. 41 | */ 42 | public function scopeToken(Builder $query, $value): Builder 43 | { 44 | return $query->where($this->getTokenColumn(), $value); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithSqlViewMigration.php: -------------------------------------------------------------------------------- 1 | down(); 16 | $this->run( 17 | $this->getUpFilename() 18 | ); 19 | } 20 | 21 | public function down() 22 | { 23 | $this->run( 24 | $this->getDownFilename() 25 | ); 26 | } 27 | 28 | protected function getUpFilename(): string 29 | { 30 | return $this->up_filename; 31 | } 32 | 33 | protected function getDownFilename(): string 34 | { 35 | return $this->down_filename; 36 | } 37 | 38 | protected function run($filename) 39 | { 40 | $path = $this->getPath($filename); 41 | 42 | if (! file_exists($path)) { 43 | throw new \Exception("$path file not found."); 44 | } 45 | 46 | $content = file_get_contents($path); 47 | 48 | DB::unprepared($content); 49 | } 50 | 51 | protected function getPath($filename): string 52 | { 53 | return $this->getStoragePath().DIRECTORY_SEPARATOR.$filename; 54 | } 55 | 56 | protected function getStoragePath(): string 57 | { 58 | return database_path('sql'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithUuid.php: -------------------------------------------------------------------------------- 1 | getTable(), $model->getUuidColumnName()) && is_null($model->{$model->getUuidColumnName()})) { 17 | $generator = $model->resolveGenerator( 18 | 'uuid', 19 | 'uuidGenerator', 20 | 'uuidGeneratorConfig' 21 | ); 22 | 23 | $model->{$model->getUuidColumnName()} = $generator->generate([ 24 | 'model' => $model, 25 | 'column' => $model->getUuidColumnName(), 26 | ]); 27 | } 28 | }); 29 | } 30 | 31 | /** 32 | * Get the route key for the model. 33 | */ 34 | public function getRouteKeyName(): string 35 | { 36 | return $this->getUuidColumnName(); 37 | } 38 | 39 | /** 40 | * Get UUID Column Name. 41 | */ 42 | public function getUuidColumnName(): string 43 | { 44 | return $this->uuid_column ?? 'uuid'; 45 | } 46 | 47 | /** 48 | * Scope a query based on uuid 49 | */ 50 | public function scopeUuid(Builder $query, $value): Builder 51 | { 52 | return $query->where($this->getUuidColumnName(), $value); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Generators/TokenGenerator.php: -------------------------------------------------------------------------------- 1 | 128, 13 | 'pool' => 'auto', // 'auto', 'alpha', 'alphanumeric', 'numeric', 'hex' 14 | 'prefix' => '', 15 | 'suffix' => '', 16 | 'uppercase' => false, 17 | ]; 18 | } 19 | 20 | public function generate(array $context = []): string 21 | { 22 | $length = $this->getConfigValue('length', 128); 23 | $pool = $this->getConfigValue('pool', 'auto'); 24 | 25 | $token = match ($pool) { 26 | 'alpha' => $this->generateWithPool($length, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 27 | 'numeric' => $this->generateWithPool($length, '0123456789'), 28 | 'hex' => substr(bin2hex(random_bytes(ceil($length / 2))), 0, $length), 29 | 'alphanumeric' => $this->generateWithPool($length, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 30 | default => Str::random($length), 31 | }; 32 | 33 | $prefix = $this->getConfigValue('prefix', ''); 34 | $suffix = $this->getConfigValue('suffix', ''); 35 | $uppercase = $this->getConfigValue('uppercase', false); 36 | 37 | $token = $prefix.$token.$suffix; 38 | 39 | return $uppercase ? strtoupper($token) : $token; 40 | } 41 | 42 | public function validate(mixed $value, array $context = []): bool 43 | { 44 | if (! is_string($value)) { 45 | return false; 46 | } 47 | 48 | $length = $this->getConfigValue('length', 128); 49 | $prefix = $this->getConfigValue('prefix', ''); 50 | $suffix = $this->getConfigValue('suffix', ''); 51 | 52 | $expectedLength = $length + strlen($prefix) + strlen($suffix); 53 | 54 | return strlen($value) === $expectedLength; 55 | } 56 | 57 | /** 58 | * Generate a random string from a specific character pool. 59 | */ 60 | protected function generateWithPool(int $length, string $characters): string 61 | { 62 | $charactersLength = strlen($characters); 63 | $randomString = ''; 64 | 65 | for ($i = 0; $i < $length; $i++) { 66 | $randomString .= $characters[random_int(0, $charactersLength - 1)]; 67 | } 68 | 69 | return $randomString; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Generators/UuidGenerator.php: -------------------------------------------------------------------------------- 1 | 'ordered', // 'ordered', 'v4', 'v1', 'v3', 'v5' 14 | 'format' => 'string', // 'string', 'binary', 'hex' 15 | 'prefix' => '', 16 | 'suffix' => '', 17 | 'namespace' => null, // For v3/v5 18 | 'name' => null, // For v3/v5 19 | ]; 20 | } 21 | 22 | public function generate(array $context = []): mixed 23 | { 24 | $version = $this->getConfigValue('version', 'ordered'); 25 | $format = $this->getConfigValue('format', 'string'); 26 | 27 | $uuid = match ($version) { 28 | 'v1' => Uuid::uuid1(), 29 | 'v3' => Uuid::uuid3( 30 | $this->getConfigValue('namespace', Uuid::NAMESPACE_DNS), 31 | $this->getConfigValue('name', Str::random()) 32 | ), 33 | 'v4' => Uuid::uuid4(), 34 | 'v5' => Uuid::uuid5( 35 | $this->getConfigValue('namespace', Uuid::NAMESPACE_DNS), 36 | $this->getConfigValue('name', Str::random()) 37 | ), 38 | 'ordered' => Str::orderedUuid(), 39 | default => Str::orderedUuid(), 40 | }; 41 | 42 | $value = match ($format) { 43 | 'binary' => $uuid->getBytes(), 44 | 'hex' => str_replace('-', '', $uuid->toString()), 45 | default => $uuid->toString(), 46 | }; 47 | 48 | $prefix = $this->getConfigValue('prefix', ''); 49 | $suffix = $this->getConfigValue('suffix', ''); 50 | 51 | return $prefix.$value.$suffix; 52 | } 53 | 54 | public function validate(mixed $value, array $context = []): bool 55 | { 56 | if (! is_string($value) && ! ($value instanceof \Stringable)) { 57 | return false; 58 | } 59 | 60 | $stringValue = (string) $value; 61 | 62 | // Remove prefix/suffix before validation 63 | $prefix = $this->getConfigValue('prefix', ''); 64 | $suffix = $this->getConfigValue('suffix', ''); 65 | 66 | if ($prefix && str_starts_with($stringValue, $prefix)) { 67 | $stringValue = substr($stringValue, strlen($prefix)); 68 | } 69 | 70 | if ($suffix && str_ends_with($stringValue, $suffix)) { 71 | $stringValue = substr($stringValue, 0, -strlen($suffix)); 72 | } 73 | 74 | return Str::isUuid($stringValue); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config/traitify.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => [ 19 | 'class' => \CleaniqueCoders\Traitify\Generators\TokenGenerator::class, 20 | 'config' => [ 21 | 'length' => 128, 22 | 'pool' => 'auto', // 'auto', 'alpha', 'alphanumeric', 'numeric', 'hex' 23 | 'prefix' => '', 24 | 'suffix' => '', 25 | 'uppercase' => false, 26 | ], 27 | ], 28 | 29 | 'uuid' => [ 30 | 'class' => \CleaniqueCoders\Traitify\Generators\UuidGenerator::class, 31 | 'config' => [ 32 | 'version' => 'ordered', // 'ordered', 'v1', 'v3', 'v4', 'v5' 33 | 'format' => 'string', // 'string', 'binary', 'hex' 34 | 'prefix' => '', 35 | 'suffix' => '', 36 | 'namespace' => null, // For v3/v5 37 | 'name' => null, // For v3/v5 38 | ], 39 | ], 40 | 41 | 'slug' => [ 42 | 'class' => \CleaniqueCoders\Traitify\Generators\SlugGenerator::class, 43 | 'config' => [ 44 | 'separator' => '-', 45 | 'language' => 'en', 46 | 'dictionary' => ['@' => 'at'], 47 | 'lowercase' => true, 48 | 'prefix' => '', 49 | 'suffix' => '', 50 | 'max_length' => null, 51 | 'unique' => false, 52 | ], 53 | ], 54 | ], 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Custom Generators 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Register custom generators that can be referenced by alias. 62 | | These can be used in your models by referencing the alias. 63 | | 64 | | Example: 65 | | 'short-token' => [ 66 | | 'class' => \CleaniqueCoders\Traitify\Generators\TokenGenerator::class, 67 | | 'config' => ['length' => 32], 68 | | ], 69 | | 70 | */ 71 | 72 | 'custom_generators' => [ 73 | // 74 | ], 75 | ]; 76 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithSlug.php: -------------------------------------------------------------------------------- 1 | getTable(), $model->getSlugColumnName()) && 17 | empty($model->{$model->getSlugColumnName()}) && 18 | ! empty($model->{$model->getSlugSourceColumnName()})) { 19 | $generator = $model->resolveGenerator( 20 | 'slug', 21 | 'slugGenerator', 22 | 'slugGeneratorConfig' 23 | ); 24 | 25 | $model->{$model->getSlugColumnName()} = $generator->generate([ 26 | 'model' => $model, 27 | 'column' => $model->getSlugColumnName(), 28 | 'source' => $model->{$model->getSlugSourceColumnName()}, 29 | ]); 30 | } 31 | }); 32 | 33 | static::updating(function (Model $model) { 34 | if (Schema::hasColumn($model->getTable(), $model->getSlugColumnName()) && 35 | $model->isDirty($model->getSlugSourceColumnName()) && 36 | empty($model->{$model->getSlugColumnName()}) && 37 | ! empty($model->{$model->getSlugSourceColumnName()})) { 38 | $generator = $model->resolveGenerator( 39 | 'slug', 40 | 'slugGenerator', 41 | 'slugGeneratorConfig' 42 | ); 43 | 44 | $model->{$model->getSlugColumnName()} = $generator->generate([ 45 | 'model' => $model, 46 | 'column' => $model->getSlugColumnName(), 47 | 'source' => $model->{$model->getSlugSourceColumnName()}, 48 | ]); 49 | } 50 | }); 51 | } 52 | 53 | /** 54 | * Get Slug Column Name. 55 | */ 56 | public function getSlugColumnName(): string 57 | { 58 | return $this->slug_column ?? 'slug'; 59 | } 60 | 61 | /** 62 | * Get the source column name for slug generation. 63 | */ 64 | public function getSlugSourceColumnName(): string 65 | { 66 | return $this->slug_source_column ?? 'name'; 67 | } 68 | 69 | /** 70 | * Scope a query to find by slug. 71 | */ 72 | public function scopeSlug(Builder $query, $value): Builder 73 | { 74 | return $query->where($this->getSlugColumnName(), $value); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Concerns/HasGeneratorResolver.php: -------------------------------------------------------------------------------- 1 | {$propertyName})) { 23 | $generatorClass = $this->{$propertyName}; 24 | 25 | if (is_string($generatorClass)) { 26 | return $this->instantiateGenerator($generatorClass, $configPropertyName); 27 | } 28 | 29 | // If it's already an instance 30 | if ($generatorClass instanceof ValueGenerator) { 31 | return $generatorClass; 32 | } 33 | } 34 | 35 | // 2. Check config for generator 36 | $configKey = "traitify.generators.{$type}"; 37 | $generatorConfig = config($configKey); 38 | 39 | if ($generatorConfig && isset($generatorConfig['class'])) { 40 | $class = $generatorConfig['class']; 41 | $config = $generatorConfig['config'] ?? []; 42 | 43 | return new $class($config); 44 | } 45 | 46 | // 3. Fallback to default generator 47 | $defaultClass = $this->getDefaultGeneratorClass($type); 48 | 49 | return new $defaultClass; 50 | } 51 | 52 | /** 53 | * Instantiate a generator with optional config from model property. 54 | */ 55 | protected function instantiateGenerator( 56 | string $class, 57 | ?string $configPropertyName = null 58 | ): ValueGenerator { 59 | $config = []; 60 | 61 | if ($configPropertyName && isset($this->{$configPropertyName})) { 62 | $config = $this->{$configPropertyName}; 63 | } 64 | 65 | return new $class($config); 66 | } 67 | 68 | /** 69 | * Get default generator class for a type. 70 | */ 71 | protected function getDefaultGeneratorClass(string $type): string 72 | { 73 | return match ($type) { 74 | 'token' => \CleaniqueCoders\Traitify\Generators\TokenGenerator::class, 75 | 'uuid' => \CleaniqueCoders\Traitify\Generators\UuidGenerator::class, 76 | 'slug' => \CleaniqueCoders\Traitify\Generators\SlugGenerator::class, 77 | default => throw new \InvalidArgumentException("Unknown generator type: {$type}"), 78 | }; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cleaniquecoders/traitify", 3 | "description": "Traitify is a Laravel package designed to streamline and enhance your development process by providing a collection of reusable traits and contracts.", 4 | "keywords": [ 5 | "Cleanique Coders", 6 | "laravel", 7 | "traitify" 8 | ], 9 | "homepage": "https://github.com/cleaniquecoders/traitify", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Nasrul Hazim Bin Mohamad", 14 | "email": "nasrulhazim.m@gmail.com", 15 | "role": "Solution Architect | Software Engineer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2|^8.3|^8.4", 20 | "spatie/laravel-package-tools": "^1.16", 21 | "illuminate/contracts": "^10.0||^11.0||^12.0" 22 | }, 23 | "require-dev": { 24 | "larastan/larastan": "^2.9|^3.7", 25 | "laravel/pint": "^1.14", 26 | "nunomaduro/collision": "^8.1.1||^7.10.0", 27 | "orchestra/testbench": "^9.5|^10.0", 28 | "pestphp/pest": "^2.34|^3.0|4.0", 29 | "pestphp/pest-plugin-arch": "^2.7|^3.0|4.0", 30 | "pestphp/pest-plugin-laravel": "^2.3|^3.0|4.0", 31 | "phpstan/extension-installer": "^1.3", 32 | "phpstan/phpstan-deprecation-rules": "^1.1|^2.0", 33 | "phpstan/phpstan-phpunit": "^1.3|^2.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "CleaniqueCoders\\Traitify\\": "src/", 38 | "CleaniqueCoders\\Traitify\\Database\\Factories\\": "database/factories/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "CleaniqueCoders\\Traitify\\Tests\\": "tests/", 44 | "Workbench\\App\\": "workbench/app/" 45 | } 46 | }, 47 | "scripts": { 48 | "post-autoload-dump": "@composer run prepare", 49 | "clear": "@php vendor/bin/testbench package:purge-traitify --ansi", 50 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 51 | "build": [ 52 | "@composer run prepare", 53 | "@php vendor/bin/testbench workbench:build --ansi" 54 | ], 55 | "start": [ 56 | "Composer\\Config::disableProcessTimeout", 57 | "@composer run build", 58 | "@php vendor/bin/testbench serve" 59 | ], 60 | "analyse": "vendor/bin/phpstan analyse", 61 | "test": "vendor/bin/pest", 62 | "test-coverage": "vendor/bin/pest --coverage", 63 | "format": "vendor/bin/pint" 64 | }, 65 | "config": { 66 | "sort-packages": true, 67 | "allow-plugins": { 68 | "pestphp/pest-plugin": true, 69 | "phpstan/extension-installer": true 70 | } 71 | }, 72 | "extra": { 73 | "laravel": { 74 | "providers": [ 75 | "CleaniqueCoders\\Traitify\\TraitifyServiceProvider" 76 | ], 77 | "aliases": { 78 | "Traitify": "CleaniqueCoders\\Traitify\\Facades\\Traitify" 79 | } 80 | } 81 | }, 82 | "minimum-stability": "dev", 83 | "prefer-stable": true 84 | } 85 | -------------------------------------------------------------------------------- /src/Generators/SlugGenerator.php: -------------------------------------------------------------------------------- 1 | '-', 13 | 'language' => 'en', 14 | 'dictionary' => ['@' => 'at'], 15 | 'lowercase' => true, 16 | 'prefix' => '', 17 | 'suffix' => '', 18 | 'max_length' => null, 19 | 'unique' => false, // Whether to ensure uniqueness 20 | ]; 21 | } 22 | 23 | public function generate(array $context = []): string 24 | { 25 | $source = $context['source'] ?? ''; 26 | 27 | if (empty($source)) { 28 | return ''; 29 | } 30 | 31 | $separator = $this->getConfigValue('separator', '-'); 32 | $language = $this->getConfigValue('language', 'en'); 33 | $dictionary = $this->getConfigValue('dictionary', ['@' => 'at']); 34 | 35 | $slug = Str::slug($source, $separator, $language, $dictionary); 36 | 37 | $lowercase = $this->getConfigValue('lowercase', true); 38 | if (! $lowercase) { 39 | // Preserve case but still apply slug transformation 40 | $slug = preg_replace('/[^A-Za-z0-9-]+/', $separator, $source); 41 | $slug = preg_replace('/'.$separator.'+/', $separator, $slug); 42 | $slug = trim($slug, $separator); 43 | } 44 | 45 | $prefix = $this->getConfigValue('prefix', ''); 46 | $suffix = $this->getConfigValue('suffix', ''); 47 | 48 | $slug = $prefix.$slug.$suffix; 49 | 50 | $maxLength = $this->getConfigValue('max_length'); 51 | if ($maxLength && strlen($slug) > $maxLength) { 52 | $slug = substr($slug, 0, $maxLength); 53 | // Trim trailing separator 54 | $slug = rtrim($slug, $separator); 55 | } 56 | 57 | // Handle uniqueness if required 58 | if ($this->getConfigValue('unique', false) && isset($context['model'])) { 59 | $slug = $this->ensureUnique($slug, $context); 60 | } 61 | 62 | return $slug; 63 | } 64 | 65 | public function validate(mixed $value, array $context = []): bool 66 | { 67 | if (! is_string($value)) { 68 | return false; 69 | } 70 | 71 | $separator = $this->getConfigValue('separator', '-'); 72 | 73 | // Check if it matches slug pattern 74 | $pattern = '/^[a-z0-9'.preg_quote($separator, '/').']+$/i'; 75 | 76 | return preg_match($pattern, $value) === 1; 77 | } 78 | 79 | /** 80 | * Ensure slug uniqueness by appending incremental numbers. 81 | */ 82 | protected function ensureUnique(string $slug, array $context): string 83 | { 84 | $model = $context['model']; 85 | $column = $context['column'] ?? 'slug'; 86 | $separator = $this->getConfigValue('separator', '-'); 87 | 88 | $originalSlug = $slug; 89 | $counter = 1; 90 | 91 | while ($model->newQuery()->where($column, $slug)->exists()) { 92 | $slug = $originalSlug.$separator.$counter; 93 | $counter++; 94 | } 95 | 96 | return $slug; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Traitify 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/cleaniquecoders/traitify.svg?style=flat-square)](https://packagist.org/packages/cleaniquecoders/traitify) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/cleaniquecoders/traitify/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/cleaniquecoders/traitify/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/cleaniquecoders/traitify/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/cleaniquecoders/traitify/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/cleaniquecoders/traitify.svg?style=flat-square)](https://packagist.org/packages/cleaniquecoders/traitify) 7 | 8 | A Laravel package that streamlines development with reusable traits, contracts, and a powerful value generator system. Reduce boilerplate, standardize behavior, and enhance your models with automatic UUID, token, and slug generation. 9 | 10 | ## ✨ Features 11 | 12 | - 🔧 **11 Reusable Traits** - UUID, Token, Slug, Meta, User, API, Search, and more 13 | - 🎨 **Customizable Generators** - Flexible token, UUID, and slug generation 14 | - ⚙️ **Three-Tier Configuration** - Model → Config → Default resolution 15 | - 🔌 **Extensible Architecture** - Create custom generators easily 16 | - 📦 **Zero Configuration** - Works out of the box with sensible defaults 17 | - ✅ **100% Tested** - Comprehensive test coverage with Pest PHP 18 | 19 | ## 📦 Installation 20 | 21 | ```bash 22 | composer require cleaniquecoders/traitify 23 | ``` 24 | 25 | ## 🚀 Quick Start 26 | 27 | ```php 28 | use CleaniqueCoders\Traitify\Concerns\InteractsWithUuid; 29 | use Illuminate\Database\Eloquent\Model; 30 | 31 | class Post extends Model 32 | { 33 | use InteractsWithUuid; 34 | 35 | // UUID automatically generated on creation 36 | } 37 | ``` 38 | 39 | ```php 40 | $post = Post::create(['title' => 'Hello World']); 41 | echo $post->uuid; // 9d9e8da7-78c3-4c9d-9f5e-5c8e4a2b1d3c 42 | ``` 43 | 44 | ## 📚 Documentation 45 | 46 | - **[Documentation Home](docs/README.md)** - Complete documentation index 47 | - **[Getting Started](docs/01-getting-started/README.md)** - Installation and setup 48 | - **[Architecture](docs/02-architecture/README.md)** - System design and patterns 49 | - **[Traits Reference](docs/03-traits/README.md)** - All available traits 50 | - **[Generators](docs/04-generators/README.md)** - Customizable value generation 51 | - **[Configuration](docs/05-configuration/README.md)** - Configuration options 52 | - **[Examples](docs/06-examples/README.md)** - Real-world usage examples 53 | - **[Advanced](docs/07-advanced/README.md)** - Extend and customize 54 | 55 | ## 🔥 Popular Use Cases 56 | 57 | ### Auto-Generate UUIDs 58 | ```php 59 | use InteractsWithUuid; 60 | 61 | protected $uuid_column = 'id'; // Use UUID as primary key 62 | ``` 63 | 64 | ### Secure API Tokens 65 | ```php 66 | use InteractsWithToken; 67 | 68 | protected $tokenGeneratorConfig = [ 69 | 'length' => 64, 70 | 'prefix' => 'sk_', 71 | 'pool' => 'hex', 72 | ]; 73 | ``` 74 | 75 | ### SEO-Friendly Slugs 76 | ```php 77 | use InteractsWithSlug; 78 | 79 | protected $slugGeneratorConfig = [ 80 | 'unique' => true, 81 | 'max_length' => 100, 82 | ]; 83 | ``` 84 | 85 | ## 🧪 Testing 86 | 87 | ```bash 88 | composer test 89 | ``` 90 | 91 | ## 📖 Available Traits 92 | 93 | | Trait | Purpose | 94 | |-------|---------| 95 | | `InteractsWithUuid` | Auto-generate UUIDs | 96 | | `InteractsWithToken` | Generate secure tokens | 97 | | `InteractsWithSlug` | Create URL-friendly slugs | 98 | | `InteractsWithMeta` | Manage JSON metadata | 99 | | `InteractsWithUser` | Auto-assign user relationships | 100 | | `InteractsWithApi` | API response formatting | 101 | | `InteractsWithSearchable` | Full-text search | 102 | | `InteractsWithDetails` | Eager load relationships | 103 | | `InteractsWithEnum` | Enum helper methods | 104 | | `InteractsWithResourceRoute` | Resource route generation | 105 | | `InteractsWithSqlViewMigration` | SQL view migrations | 106 | 107 | ## 🤝 Contributing 108 | 109 | Contributions are welcome! Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 110 | 111 | ## 🔒 Security 112 | 113 | If you discover any security issues, please review our [security policy](../../security/policy). 114 | 115 | ## 📝 Changelog 116 | 117 | Please see [CHANGELOG](CHANGELOG.md) for recent changes. 118 | 119 | ## 👥 Credits 120 | 121 | - [Nasrul Hazim Bin Mohamad](https://github.com/nasrulhazim) 122 | - [All Contributors](../../contributors) 123 | 124 | ## 📄 License 125 | 126 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 127 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithTags.php: -------------------------------------------------------------------------------- 1 | getTable(), $model->getTagsColumnName())) { 18 | $tags = $model->{$model->getTagsColumnName()}; 19 | 20 | if (is_string($tags)) { 21 | $model->{$model->getTagsColumnName()} = $model->normalizeTags($tags); 22 | } 23 | } 24 | }); 25 | } 26 | 27 | /** 28 | * Get the tags column name. 29 | */ 30 | public function getTagsColumnName(): string 31 | { 32 | return $this->tags_column ?? 'tags'; 33 | } 34 | 35 | /** 36 | * Get tags as array. 37 | */ 38 | public function getTags(): array 39 | { 40 | $tags = $this->{$this->getTagsColumnName()} ?? []; 41 | 42 | return is_string($tags) ? json_decode($tags, true) ?? [] : (array) $tags; 43 | } 44 | 45 | /** 46 | * Set tags. 47 | * 48 | * @param mixed $tags 49 | * @return $this 50 | */ 51 | public function setTags($tags): self 52 | { 53 | $this->{$this->getTagsColumnName()} = $this->normalizeTags($tags); 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * Add one or multiple tags. 60 | * 61 | * @param mixed $tags 62 | * @return $this 63 | */ 64 | public function addTags($tags): self 65 | { 66 | $existingTags = $this->getTags(); 67 | $newTags = $this->normalizeTags($tags); 68 | 69 | $mergedTags = array_unique(array_merge($existingTags, $newTags)); 70 | 71 | $this->{$this->getTagsColumnName()} = $mergedTags; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Remove one or multiple tags. 78 | * 79 | * @param mixed $tags 80 | * @return $this 81 | */ 82 | public function removeTags($tags): self 83 | { 84 | $existingTags = $this->getTags(); 85 | $tagsToRemove = $this->normalizeTags($tags); 86 | 87 | $remainingTags = array_values(array_diff($existingTags, $tagsToRemove)); 88 | 89 | $this->{$this->getTagsColumnName()} = $remainingTags; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Clear all tags. 96 | * 97 | * @return $this 98 | */ 99 | public function clearTags(): self 100 | { 101 | $this->{$this->getTagsColumnName()} = []; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Check if the model has any of the given tags. 108 | * 109 | * @param mixed $tags 110 | */ 111 | public function hasTag($tags): bool 112 | { 113 | $existingTags = $this->getTags(); 114 | $tagsToCheck = $this->normalizeTags($tags); 115 | 116 | foreach ($tagsToCheck as $tag) { 117 | if (in_array($tag, $existingTags)) { 118 | return true; 119 | } 120 | } 121 | 122 | return false; 123 | } 124 | 125 | /** 126 | * Check if the model has all of the given tags. 127 | * 128 | * @param mixed $tags 129 | */ 130 | public function hasAllTags($tags): bool 131 | { 132 | $existingTags = $this->getTags(); 133 | $tagsToCheck = $this->normalizeTags($tags); 134 | 135 | foreach ($tagsToCheck as $tag) { 136 | if (! in_array($tag, $existingTags)) { 137 | return false; 138 | } 139 | } 140 | 141 | return true; 142 | } 143 | 144 | /** 145 | * Scope a query to models with any of the given tags. 146 | */ 147 | public function scopeWithAnyTags(Builder $query, $tags): Builder 148 | { 149 | $tags = $this->normalizeTags($tags); 150 | $column = $this->getTagsColumnName(); 151 | 152 | return $query->where(function (Builder $query) use ($tags, $column) { 153 | foreach ($tags as $tag) { 154 | $query->orWhereJsonContains($column, $tag); 155 | } 156 | }); 157 | } 158 | 159 | /** 160 | * Scope a query to models with all of the given tags. 161 | */ 162 | public function scopeWithAllTags(Builder $query, $tags): Builder 163 | { 164 | $tags = $this->normalizeTags($tags); 165 | $column = $this->getTagsColumnName(); 166 | 167 | foreach ($tags as $tag) { 168 | $query->whereJsonContains($column, $tag); 169 | } 170 | 171 | return $query; 172 | } 173 | 174 | /** 175 | * Scope a query to models without any of the given tags. 176 | */ 177 | public function scopeWithoutTags(Builder $query, $tags): Builder 178 | { 179 | $tags = $this->normalizeTags($tags); 180 | $column = $this->getTagsColumnName(); 181 | 182 | foreach ($tags as $tag) { 183 | $query->whereJsonDoesntContain($column, $tag); 184 | } 185 | 186 | return $query; 187 | } 188 | 189 | /** 190 | * Scope a query to models without all of the given tags. 191 | */ 192 | public function scopeWithoutAnyTags(Builder $query, $tags): Builder 193 | { 194 | $tags = $this->normalizeTags($tags); 195 | $column = $this->getTagsColumnName(); 196 | 197 | return $query->where(function (Builder $query) use ($tags, $column) { 198 | foreach ($tags as $tag) { 199 | $query->whereJsonDoesntContain($column, $tag); 200 | } 201 | }); 202 | } 203 | 204 | /** 205 | * Scope a query to models that have tags. 206 | */ 207 | public function scopeHasTags(Builder $query): Builder 208 | { 209 | $column = $this->getTagsColumnName(); 210 | 211 | return $query->whereNotNull($column) 212 | ->where($column, '!=', '[]') 213 | ->where($column, '!=', ''); 214 | } 215 | 216 | /** 217 | * Scope a query to models that don't have tags. 218 | */ 219 | public function scopeHasNoTags(Builder $query): Builder 220 | { 221 | $column = $this->getTagsColumnName(); 222 | 223 | return $query->where(function (Builder $query) use ($column) { 224 | $query->whereNull($column) 225 | ->orWhere($column, '[]') 226 | ->orWhere($column, ''); 227 | }); 228 | } 229 | 230 | /** 231 | * Normalize tags input to an array. 232 | * 233 | * @param mixed $tags 234 | */ 235 | protected function normalizeTags($tags): array 236 | { 237 | if (is_null($tags)) { 238 | return []; 239 | } 240 | 241 | if (is_string($tags)) { 242 | return array_values(array_filter(array_map('trim', explode(',', $tags)))); 243 | } 244 | 245 | if (is_array($tags)) { 246 | return array_values(array_filter($tags)); 247 | } 248 | 249 | return (array) $tags; 250 | } 251 | 252 | /** 253 | * Cast tags attribute to array. 254 | */ 255 | protected function initializeInteractsWithTags() 256 | { 257 | $column = $this->getTagsColumnName(); 258 | 259 | if (! isset($this->casts[$column])) { 260 | $this->casts[$column] = 'array'; 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `Traitify` will be documented in this file. 4 | 5 | ## Customizable Value Generator System & Updating Documentation - 2025-12-10 6 | 7 | #### Documentation 8 | 9 | All documentations are updated and moved to [`docs/`](https://github.com/cleaniquecoders/traitify/tree/main/docs) directory. 10 | 11 | #### Customizable Value Generator System 12 | 13 | Introduced a flexible, extensible generator system for tokens, UUIDs, and slugs with three-tier configuration support. 14 | 15 | ##### New Components 16 | 17 | **Contracts & Interfaces:** 18 | 19 | - `ValueGenerator` interface - Defines standard contract for all generators 20 | - Supports `generate()`, `validate()`, `getConfig()`, and `setConfig()` methods 21 | 22 | **Generator Classes:** 23 | 24 | - **TokenGenerator** - Configurable token generation 25 | 26 | - Multiple character pools: `auto`, `alpha`, `alphanumeric`, `numeric`, `hex` 27 | - Configurable length (default: 128) 28 | - Prefix/suffix support 29 | - Uppercase option 30 | 31 | - **UuidGenerator** - Multiple UUID version support 32 | 33 | - Versions: `ordered` (default), `v1`, `v3`, `v4`, `v5` 34 | - Output formats: `string`, `binary`, `hex` 35 | - Prefix/suffix support 36 | - Custom namespace/name for v3/v5 37 | 38 | - **SlugGenerator** - Advanced slug generation 39 | 40 | - Custom separators 41 | - Language support 42 | - Dictionary mappings 43 | - Max length constraints 44 | - Uniqueness checking 45 | - Case preservation option 46 | 47 | 48 | **Configuration System:** 49 | 50 | - New `config/traitify.php` configuration file 51 | - Three-tier resolution: Model Property → Config File → Default 52 | - Per-model generator customization via properties 53 | - App-wide defaults via config file 54 | 55 | **Architecture:** 56 | 57 | - `AbstractValueGenerator` - Base class with shared functionality 58 | - `HasGeneratorResolver` trait - Generator resolution logic 59 | - Dot notation config access support 60 | 61 | ### 🔄 Enhancements 62 | 63 | #### Refactored Traits (Backward Compatible) 64 | 65 | - `InteractsWithToken` - Now uses configurable `TokenGenerator` 66 | - `InteractsWithUuid` - Now uses configurable `UuidGenerator` 67 | - `InteractsWithSlug` - Now uses configurable `SlugGenerator` 68 | 69 | #### Service Provider 70 | 71 | - Added config file publishing support via `hasConfigFile()` 72 | - Use `php artisan vendor:publish --tag=traitify-config` to publish 73 | 74 | ### 📚 Usage Examples 75 | 76 | #### App-wide Configuration 77 | 78 | ```php 79 | // config/traitify.php 80 | 'generators' => [ 81 | 'token' => [ 82 | 'class' => \CleaniqueCoders\Traitify\Generators\TokenGenerator::class, 83 | 'config' => [ 84 | 'length' => 64, 85 | 'prefix' => 'API_', 86 | 'uppercase' => true, 87 | ], 88 | ], 89 | 'uuid' => [ 90 | 'class' => \CleaniqueCoders\Traitify\Generators\UuidGenerator::class, 91 | 'config' => [ 92 | 'version' => 'v4', 93 | 'format' => 'string', 94 | ], 95 | ], 96 | 'slug' => [ 97 | 'class' => \CleaniqueCoders\Traitify\Generators\SlugGenerator::class, 98 | 'config' => [ 99 | 'separator' => '_', 100 | 'max_length' => 100, 101 | 'unique' => true, 102 | ], 103 | ], 104 | ], 105 | 106 | ``` 107 | Per-Model Customization 108 | 109 | ```php 110 | use CleaniqueCoders\Traitify\Concerns\InteractsWithToken; 111 | use Illuminate\Database\Eloquent\Model; 112 | 113 | class ApiKey extends Model 114 | { 115 | use InteractsWithToken; 116 | 117 | // Option 1: Use a custom generator class 118 | protected $tokenGenerator = \App\Generators\MyCustomTokenGenerator::class; 119 | 120 | // Option 2: Configure the default generator for this model 121 | protected $tokenGeneratorConfig = [ 122 | 'length' => 32, 123 | 'pool' => 'hex', 124 | 'prefix' => 'sk_', 125 | ]; 126 | } 127 | 128 | ``` 129 | Custom Generator Implementation 130 | 131 | ```php 132 | use CleaniqueCoders\Traitify\Generators\AbstractValueGenerator; 133 | 134 | class MyCustomTokenGenerator extends AbstractValueGenerator 135 | { 136 | protected function getDefaultConfig(): array 137 | { 138 | return [ 139 | 'format' => 'custom', 140 | 'length' => 40, 141 | ]; 142 | } 143 | 144 | public function generate(array $context = []): mixed 145 | { 146 | // Your custom generation logic 147 | $length = $this->getConfigValue('length', 40); 148 | return bin2hex(random_bytes($length / 2)); 149 | } 150 | 151 | public function validate(mixed $value, array $context = []): bool 152 | { 153 | // Your validation logic 154 | return is_string($value) && strlen($value) === $this->getConfigValue('length', 40); 155 | } 156 | } 157 | 158 | ``` 159 | 🔒 Backward Compatibility 160 | 161 | 100% backward compatible - No breaking changes: 162 | 163 | - ✅ Existing models work without any changes 164 | - ✅ Default behavior unchanged (Token: 128 chars, UUID: ordered, Slug: from name) 165 | - ✅ All column customization properties still work ($token_column, $uuid_column, etc.) 166 | - ✅ No migration required 167 | - ✅ Opt-in enhancement - use new features when you need them 168 | 169 | 🚀 Upgrade Guide 170 | 171 | No upgrade steps required! The changes are fully backward compatible. 172 | 173 | Optional: Publish the config file to customize generators app-wide: 174 | 175 | ```bash 176 | php artisan vendor:publish --tag=traitify-config 177 | 178 | ``` 179 | This will create `config/traitify.php` in your Laravel application. 180 | 181 | ## Added Interact with Tag - 2025-11-01 182 | 183 | See [here](https://github.com/cleaniquecoders/traitify/blob/main/docs/interacts-with-tags.md) for more details. 184 | 185 | ## Added Interaction with Slug - 2025-10-10 186 | 187 | **Full Changelog**: https://github.com/cleaniquecoders/traitify/compare/1.1.0...1.2.0 188 | 189 | ## 1.1.0 - 2025-05-01 190 | 191 | **Full Changelog**: https://github.com/cleaniquecoders/traitify/compare/v1.0.2...1.1.0 192 | 193 | ## v1.0.2 - 2024-11-27 194 | 195 | - Added #1 196 | 197 | **Full Changelog**: https://github.com/cleaniquecoders/traitify/compare/v1.0.1...v1.0.2 198 | 199 | ## v1.0.1 - 2024-10-16 200 | 201 | ### Traitify v1.0.1 Release Notes 202 | 203 | **Release Date**: 15th October 2024 204 | 205 | This patch release focuses on improvements to the **`InteractsWithSqlViewMigration`** trait, which simplifies SQL view management in Laravel migrations. 206 | 207 | #### Key Updates in v1.0.1: 208 | 209 | 1. **InteractsWithSqlViewMigration Trait**: 210 | - Automates the process of managing SQL views during migrations. 211 | - Supports the use of external SQL files to create and drop views. 212 | - Ensures better error handling with exceptions when SQL files are missing. 213 | - Customizable filenames for both creating and dropping views via `getUpFilename()` and `getDownFilename()` methods. 214 | 215 | 216 | #### Installation: 217 | 218 | To update to v1.0.1, run: 219 | 220 | ```bash 221 | composer update cleaniquecoders/traitify 222 | 223 | 224 | 225 | 226 | 227 | 228 | ``` 229 | #### Documentation: 230 | 231 | For more details on how to use the `InteractsWithSqlViewMigration` trait, please refer to the [GitHub repository](https://github.com/cleaniquecoders/traitify). 232 | 233 | 234 | --- 235 | 236 | This update enhances how SQL views are handled in migrations, making it easier to maintain and organize SQL scripts in your Laravel projects. 237 | 238 | **Full Changelog**: https://github.com/cleaniquecoders/traitify/compare/v1.0.0...v1.0.1 239 | 240 | ## v1.0.0 - 2024-10-15 241 | 242 | ### Release Notes for Traitify v1.0.0 243 | 244 | **Release Date**: 15th October 2024 245 | 246 | We are excited to announce the first official release of **Traitify** (v1.0.0), a Laravel package that provides a set of reusable traits and contracts to streamline application development and enforce best practices. 247 | 248 | #### New Features: 249 | 250 | 1. **InteractsWithUuid Trait**: 251 | 252 | - Automatically generates UUIDs for models during creation. 253 | - Supports custom UUID column names. 254 | - Provides query scope for filtering models by UUID. 255 | 256 | 2. **InteractsWithUser Trait**: 257 | 258 | - Automatically assigns the authenticated user ID to models during creation. 259 | - Supports custom user ID column names. 260 | - Works seamlessly with Laravel's `Auth` facade. 261 | 262 | 3. **InteractsWithToken Trait**: 263 | 264 | - Automatically generates random 128-character tokens for models. 265 | - Supports custom token column names. 266 | - Provides query scope for filtering models by token. 267 | 268 | 4. **InteractsWithSearchable Trait**: 269 | 270 | - Adds case-insensitive search functionality to models. 271 | - Supports searching across single or multiple fields. 272 | 273 | 5. **InteractsWithResourceRoute Trait**: 274 | 275 | - Helps in generating resource URLs (e.g., `index`, `show`) for models. 276 | - Automatically derives route base names from model names. 277 | 278 | 6. **InteractsWithMeta Trait**: 279 | 280 | - Manages meta fields dynamically in models. 281 | - Automatically adds casts for the `meta` attribute as an array. 282 | - Supports default meta values. 283 | 284 | 7. **InteractsWithEnum Trait**: 285 | 286 | - Provides methods to handle enum values, labels, and options with descriptions. 287 | - Supports usage in select inputs for better UX. 288 | 289 | 8. **InteractsWithDetails Trait**: 290 | 291 | - Allows models to define related details and apply eager loading. 292 | - Provides a query scope to load related details efficiently. 293 | 294 | 9. **InteractsWithApi Trait**: 295 | 296 | - Simplifies API response structure with methods for data, messages, and status codes. 297 | - Supports customization of API responses. 298 | 299 | 300 | #### Contracts: 301 | 302 | 1. **Builder Contract**: 303 | 304 | - Defines a `build()` method that returns the instance of the class implementing it. 305 | 306 | 2. **Execute Contract**: 307 | 308 | - Defines an `execute()` method that returns the instance of the class implementing it. 309 | 310 | 3. **Menu Contract**: 311 | 312 | - Defines a `menus()` method that returns a collection of menu items. 313 | 314 | 4. **Processor Contract**: 315 | 316 | - Defines a `process()` method that returns the instance of the class implementing it. 317 | 318 | 319 | #### Improvements & Enhancements: 320 | 321 | - Enhanced unit tests for each trait and contract, ensuring reliable functionality. 322 | - Compatibility with Laravel's core features like UUIDs, meta fields, token management, and API responses. 323 | - Designed to be modular, flexible, and easy to extend. 324 | 325 | #### Installation: 326 | 327 | You can install the package via Composer: 328 | 329 | ```bash 330 | composer require cleaniquecoders/traitify 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | ``` 339 | #### Documentation: 340 | 341 | Full documentation and examples are available in the repository’s README. 342 | 343 | **Full Changelog**: https://github.com/cleaniquecoders/traitify/commits/v1.0.0 344 | --------------------------------------------------------------------------------