├── .gitattributes ├── src ├── Seeders │ ├── stubs │ │ └── seeder.stub │ ├── Seeder.php │ └── SeederCreator.php ├── Model │ ├── Events │ │ ├── Saved.php │ │ ├── Booted.php │ │ ├── Booting.php │ │ ├── Created.php │ │ ├── Creating.php │ │ ├── Deleted.php │ │ ├── Deleting.php │ │ ├── Restored.php │ │ ├── Saving.php │ │ ├── Updated.php │ │ ├── Updating.php │ │ ├── Restoring.php │ │ ├── Retrieved.php │ │ ├── ForceDeleted.php │ │ ├── ForceDeleting.php │ │ └── Event.php │ ├── MassAssignmentException.php │ ├── Booted.php │ ├── GlobalScope.php │ ├── TraitInitializers.php │ ├── Scope.php │ ├── IgnoreOnTouch.php │ ├── Relations │ │ ├── Pivot.php │ │ ├── Constraint.php │ │ ├── HasMany.php │ │ ├── MorphMany.php │ │ ├── Concerns │ │ │ └── SupportsDefaultModels.php │ │ ├── HasOne.php │ │ ├── MorphOne.php │ │ ├── HasOneThrough.php │ │ ├── MorphOneOrMany.php │ │ └── MorphPivot.php │ ├── CollectionMeta.php │ ├── ModelMeta.php │ ├── RelationNotFoundException.php │ ├── Casts │ │ ├── ArrayObject.php │ │ └── AsArrayObject.php │ ├── JsonEncodingException.php │ ├── EnumCollector.php │ ├── Concerns │ │ ├── HasUuids.php │ │ ├── HasUlids.php │ │ ├── CamelCase.php │ │ ├── HasGlobalScopes.php │ │ ├── HasTimestamps.php │ │ ├── HidesAttributes.php │ │ └── GuardsAttributes.php │ ├── CastsValue.php │ ├── ModelNotFoundException.php │ ├── Register.php │ └── SoftDeletingScope.php ├── Query │ ├── JoinLateralClause.php │ ├── IndexHint.php │ ├── Expression.php │ ├── JsonExpression.php │ ├── Processors │ │ ├── MySqlProcessor.php │ │ └── Processor.php │ └── JoinClause.php ├── Events │ ├── TransactionBeginning.php │ ├── TransactionCommitted.php │ ├── TransactionRolledBack.php │ ├── StatementPrepared.php │ ├── ConnectionEvent.php │ └── QueryExecuted.php ├── Exception │ ├── InvalidArgumentException.php │ ├── UniqueConstraintViolationException.php │ ├── RecordsNotFoundException.php │ ├── InvalidBindingException.php │ ├── MultipleColumnsSelectedException.php │ ├── ClassMorphViolationException.php │ ├── InvalidCastException.php │ ├── MultipleRecordsFoundException.php │ └── QueryException.php ├── Migrations │ ├── stubs │ │ ├── blank.stub │ │ ├── update.stub │ │ └── create.stub │ ├── Migration.php │ └── MigrationRepositoryInterface.php ├── DBAL │ ├── MySqlDriver.php │ ├── Concerns │ │ └── ConnectsToDatabase.php │ └── Connection.php ├── Connectors │ ├── ConnectorInterface.php │ └── Connector.php ├── Commands │ ├── Annotations │ │ └── RewriteReturnType.php │ ├── stubs │ │ └── Model.stub │ ├── Ast │ │ ├── AbstractVisitor.php │ │ ├── ModelRewriteConnectionVisitor.php │ │ ├── ModelRewriteInheritanceVisitor.php │ │ ├── ModelRewriteTimestampsVisitor.php │ │ ├── ModelRewriteSoftDeletesVisitor.php │ │ └── ModelRewriteGetterSetterVisitor.php │ ├── ModelData.php │ ├── Migrations │ │ ├── TableGuesser.php │ │ ├── InstallCommand.php │ │ ├── BaseCommand.php │ │ ├── ResetCommand.php │ │ ├── RollbackCommand.php │ │ ├── StatusCommand.php │ │ ├── MigrateCommand.php │ │ ├── FreshCommand.php │ │ ├── GenMigrateCommand.php │ │ └── RefreshCommand.php │ ├── Seeders │ │ ├── BaseCommand.php │ │ ├── SeedCommand.php │ │ └── GenSeederCommand.php │ ├── CommandCollector.php │ └── ModelOption.php ├── ConnectionResolverInterface.php ├── Concerns │ └── ExplainsQueries.php ├── MySqlBitConnection.php ├── DetectsDeadlocks.php ├── Schema │ ├── ForeignKeyDefinition.php │ ├── Column.php │ ├── ForeignIdColumnDefinition.php │ ├── ColumnDefinition.php │ ├── Grammars │ │ └── RenameColumn.php │ └── Schema.php ├── ConnectionResolver.php ├── DetectsLostConnections.php ├── MySqlConnection.php └── ConnectionInterface.php ├── LICENSE └── composer.json /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /.github export-ignore 3 | /types export-ignore 4 | -------------------------------------------------------------------------------- /src/Seeders/stubs/seeder.stub: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 16 | $table->datetimes(); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::dropIfExists('DummyTable'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/Commands/Ast/AbstractVisitor.php: -------------------------------------------------------------------------------- 1 | class)) { 27 | return new Collection(); 28 | } 29 | 30 | return $this->class::findMany($this->keys); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Migrations/Migration.php: -------------------------------------------------------------------------------- 1 | connection; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Concerns/ExplainsQueries.php: -------------------------------------------------------------------------------- 1 | toSql(); 29 | 30 | $bindings = $this->getBindings(); 31 | 32 | $explanation = $this->getConnection()->select('EXPLAIN ' . $sql, $bindings); 33 | 34 | return new Collection($explanation); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/DBAL/Concerns/ConnectsToDatabase.php: -------------------------------------------------------------------------------- 1 | model = $class; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exception/InvalidCastException.php: -------------------------------------------------------------------------------- 1 | connectionName = $connection->getName(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Model/ModelMeta.php: -------------------------------------------------------------------------------- 1 | key = $key; 30 | } 31 | 32 | public function uncompress() 33 | { 34 | if (is_null($this->key)) { 35 | return new $this->class(); 36 | } 37 | return $this->class::find($this->key); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/MySqlBitConnection.php: -------------------------------------------------------------------------------- 1 | $value) { 23 | $type = PDO::PARAM_STR; 24 | if (in_array($value, [0, 1], true)) { 25 | $type = PDO::PARAM_INT; 26 | } 27 | $statement->bindValue( 28 | is_string($key) ? $key : $key + 1, 29 | $value, 30 | $type 31 | ); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Seeders/Seeder.php: -------------------------------------------------------------------------------- 1 | connection; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Query/Expression.php: -------------------------------------------------------------------------------- 1 | getValue(); 33 | } 34 | 35 | /** 36 | * Get the value of the expression. 37 | */ 38 | public function getValue() 39 | { 40 | return $this->value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Commands/ModelData.php: -------------------------------------------------------------------------------- 1 | columns; 24 | } 25 | 26 | public function setColumns(array $columns): static 27 | { 28 | $this->columns = $columns; 29 | return $this; 30 | } 31 | 32 | public function getClass(): string 33 | { 34 | return $this->class; 35 | } 36 | 37 | public function setClass(string $class): static 38 | { 39 | $this->class = $class; 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Exception/MultipleRecordsFoundException.php: -------------------------------------------------------------------------------- 1 | count = $count; 28 | 29 | parent::__construct("{$count} records were found.", $code, $previous); 30 | } 31 | 32 | /** 33 | * Get the number of records found. 34 | */ 35 | public function getCount(): int 36 | { 37 | return $this->count; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Model/Relations/Constraint.php: -------------------------------------------------------------------------------- 1 | getMessage(); 26 | 27 | return Str::contains($message, [ 28 | 'Deadlock found when trying to get lock', 29 | 'deadlock detected', 30 | 'The database file is locked', 31 | 'database is locked', 32 | 'database table is locked', 33 | 'A table in the database is locked', 34 | 'has been chosen as the deadlock victim', 35 | 'Lock wait timeout exceeded; try restarting transaction', 36 | 'WSREP detected deadlock/conflict and aborted the transaction. Try restarting the transaction', 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Model/RelationNotFoundException.php: -------------------------------------------------------------------------------- 1 | model = $model; 47 | $instance->relation = $relation; 48 | 49 | return $instance; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Model/Casts/ArrayObject.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class ArrayObject extends BaseArrayObject implements Arrayable, JsonSerializable 26 | { 27 | /** 28 | * Get a collection containing the underlying array. 29 | */ 30 | public function collect(): Collection 31 | { 32 | return new Collection($this->getArrayCopy()); 33 | } 34 | 35 | /** 36 | * Get the instance as an array. 37 | */ 38 | public function toArray(): array 39 | { 40 | return $this->getArrayCopy(); 41 | } 42 | 43 | /** 44 | * Get the array that should be JSON serialized. 45 | */ 46 | public function jsonSerialize(): array 47 | { 48 | return $this->getArrayCopy(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Commands/Migrations/TableGuesser.php: -------------------------------------------------------------------------------- 1 | getKey() . '] to JSON: ' . $message); 29 | } 30 | 31 | /** 32 | * Create a new JSON encoding exception for an attribute. 33 | * 34 | * @param string $message 35 | * @param mixed $model 36 | * @param mixed $key 37 | * @return static 38 | */ 39 | public static function forAttribute($model, $key, $message) 40 | { 41 | $class = get_class($model); 42 | 43 | return new static("Unable to encode attribute [{$key}] for model [{$class}] to JSON: {$message}."); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Events/QueryExecuted.php: -------------------------------------------------------------------------------- 1 | connectionName = $connection->getName(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Model/Events/Event.php: -------------------------------------------------------------------------------- 1 | method = $method ?? lcfirst(class_basename(static::class)); 32 | } 33 | 34 | public function handle() 35 | { 36 | if (method_exists($this->getModel(), $this->getMethod())) { 37 | $this->getModel()->{$this->getMethod()}($this); 38 | } 39 | 40 | return $this; 41 | } 42 | 43 | public function getMethod(): string 44 | { 45 | return $this->method; 46 | } 47 | 48 | public function getModel(): Model 49 | { 50 | return $this->model; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Query/JsonExpression.php: -------------------------------------------------------------------------------- 1 | getJsonBindingParameter($value) 26 | ); 27 | } 28 | 29 | /** 30 | * Translate the given value into the appropriate JSON binding parameter. 31 | * 32 | * @return string 33 | * @throws InvalidArgumentException 34 | */ 35 | protected function getJsonBindingParameter(mixed $value) 36 | { 37 | if ($value instanceof Expression) { 38 | return $value->getValue(); 39 | } 40 | 41 | $type = gettype($value); 42 | return match ($type) { 43 | 'boolean' => $value ? 'true' : 'false', 44 | 'NULL', 'integer', 'double', 'string', 'object', 'array' => '?', 45 | default => throw new InvalidArgumentException("JSON value is of illegal type: {$type}") 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Schema/ForeignKeyDefinition.php: -------------------------------------------------------------------------------- 1 | onUpdate('cascade'); 33 | } 34 | 35 | /** 36 | * Indicate that deletes should set the foreign key value to null. 37 | */ 38 | public function nullOnDelete(): static 39 | { 40 | return $this->onDelete('set null'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Model/EnumCollector.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected static array $reflections = []; 25 | 26 | public static function get(string $class): ReflectionEnum 27 | { 28 | if (isset(static::$reflections[$class])) { 29 | return static::$reflections[$class]; 30 | } 31 | 32 | return static::$reflections[$class] = new ReflectionEnum($class); 33 | } 34 | 35 | public static function has(string $class): bool 36 | { 37 | return isset(static::$reflections[$class]); 38 | } 39 | 40 | public static function getEnumCaseFromValue(string $class, int|string $value): BackedEnum|UnitEnum 41 | { 42 | $ref = self::get($class); 43 | if ($ref->isBacked()) { 44 | if ($ref->getBackingType()?->getName() === 'int') { 45 | return $class::from((int) $value); 46 | } 47 | 48 | return $class::from((string) $value); 49 | } 50 | 51 | return constant($class . '::' . $value); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Commands/Seeders/BaseCommand.php: -------------------------------------------------------------------------------- 1 | seed->paths(), 27 | [$this->getSeederPath()] 28 | ); 29 | } 30 | 31 | /** 32 | * Get seeder path (either specified by '--path' option or default location). 33 | */ 34 | protected function getSeederPath(): string 35 | { 36 | if (! is_null($targetPath = $this->input->getOption('path'))) { 37 | return ! $this->usingRealPath() 38 | ? BASE_PATH . '/' . $targetPath 39 | : $targetPath; 40 | } 41 | 42 | return BASE_PATH . DIRECTORY_SEPARATOR . 'seeders'; 43 | } 44 | 45 | /** 46 | * Determine if the given path(s) are pre-resolved "real" paths. 47 | */ 48 | protected function usingRealPath(): bool 49 | { 50 | return $this->input->hasOption('realpath') && $this->input->getOption('realpath'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Commands/CommandCollector.php: -------------------------------------------------------------------------------- 1 | setDescription('Create the migration repository'); 27 | } 28 | 29 | /** 30 | * Handle the current command. 31 | */ 32 | public function handle() 33 | { 34 | $this->repository->setSource($this->input->getOption('database')); 35 | 36 | $this->repository->createRepository(); 37 | 38 | $this->output->writeln('[INFO] Migration table created successfully.'); 39 | } 40 | 41 | /** 42 | * Get the console command options. 43 | * 44 | * @return array 45 | */ 46 | protected function getOptions() 47 | { 48 | return [ 49 | ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Model/Concerns/HasUuids.php: -------------------------------------------------------------------------------- 1 | getKeyName()]; 37 | } 38 | 39 | /** 40 | * Get the auto-incrementing key type. 41 | * 42 | * @return string 43 | */ 44 | public function getKeyType() 45 | { 46 | if (in_array($this->getKeyName(), $this->uniqueIds())) { 47 | return 'string'; 48 | } 49 | 50 | return $this->keyType; 51 | } 52 | 53 | /** 54 | * Get the value indicating whether the IDs are incrementing. 55 | * 56 | * @return bool 57 | */ 58 | public function getIncrementing() 59 | { 60 | if (in_array($this->getKeyName(), $this->uniqueIds())) { 61 | return false; 62 | } 63 | 64 | return $this->incrementing; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Model/Concerns/HasUlids.php: -------------------------------------------------------------------------------- 1 | getKeyName()]; 37 | } 38 | 39 | /** 40 | * Get the auto-incrementing key type. 41 | * 42 | * @return string 43 | */ 44 | public function getKeyType() 45 | { 46 | if (in_array($this->getKeyName(), $this->uniqueIds())) { 47 | return 'string'; 48 | } 49 | 50 | return $this->keyType; 51 | } 52 | 53 | /** 54 | * Get the value indicating whether the IDs are incrementing. 55 | * 56 | * @return bool 57 | */ 58 | public function getIncrementing() 59 | { 60 | if (in_array($this->getKeyName(), $this->uniqueIds())) { 61 | return false; 62 | } 63 | 64 | return $this->incrementing; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Schema/Column.php: -------------------------------------------------------------------------------- 1 | schema; 32 | } 33 | 34 | public function getTable(): string 35 | { 36 | return $this->table; 37 | } 38 | 39 | public function getName(): string 40 | { 41 | return $this->name; 42 | } 43 | 44 | public function getPosition(): int 45 | { 46 | return $this->position; 47 | } 48 | 49 | public function getDefault(): mixed 50 | { 51 | return $this->default; 52 | } 53 | 54 | public function isNullable(): bool 55 | { 56 | return $this->isNullable; 57 | } 58 | 59 | public function getType(): string 60 | { 61 | return $this->type; 62 | } 63 | 64 | public function getComment(): string 65 | { 66 | return $this->comment; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Model/Casts/AsArrayObject.php: -------------------------------------------------------------------------------- 1 | , iterable> 24 | */ 25 | public static function castUsing(): CastsAttributes 26 | { 27 | return new class implements CastsAttributes { 28 | public function get($model, $key, $value, $attributes) 29 | { 30 | if (! isset($attributes[$key])) { 31 | return; 32 | } 33 | 34 | $data = Json::decode($attributes[$key]); 35 | 36 | return is_array($data) ? new ArrayObject($data, ArrayObject::ARRAY_AS_PROPS) : null; 37 | } 38 | 39 | public function set($model, $key, $value, $attributes): array 40 | { 41 | return [$key => Json::encode($value)]; 42 | } 43 | 44 | public function serialize($model, string $key, $value, array $attributes) 45 | { 46 | return $value->getArrayCopy(); 47 | } 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Model/Relations/HasMany.php: -------------------------------------------------------------------------------- 1 | > 22 | */ 23 | class HasMany extends HasOneOrMany 24 | { 25 | /** 26 | * Get the results of the relationship. 27 | */ 28 | public function getResults() 29 | { 30 | return $this->query->get(); 31 | } 32 | 33 | /** 34 | * Initialize the relation on a set of models. 35 | * 36 | * @param string $relation 37 | * @return array 38 | */ 39 | public function initRelation(array $models, $relation) 40 | { 41 | foreach ($models as $model) { 42 | $model->setRelation($relation, $this->related->newCollection()); 43 | } 44 | 45 | return $models; 46 | } 47 | 48 | /** 49 | * Match the eagerly loaded results to their parents. 50 | * 51 | * @param string $relation 52 | * @return array 53 | */ 54 | public function match(array $models, Collection $results, $relation) 55 | { 56 | return $this->matchMany($models, $results, $relation); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Model/Relations/MorphMany.php: -------------------------------------------------------------------------------- 1 | > 22 | */ 23 | class MorphMany extends MorphOneOrMany 24 | { 25 | /** 26 | * Get the results of the relationship. 27 | */ 28 | public function getResults() 29 | { 30 | return $this->query->get(); 31 | } 32 | 33 | /** 34 | * Initialize the relation on a set of models. 35 | * 36 | * @param string $relation 37 | * @return array 38 | */ 39 | public function initRelation(array $models, $relation) 40 | { 41 | foreach ($models as $model) { 42 | $model->setRelation($relation, $this->related->newCollection()); 43 | } 44 | 45 | return $models; 46 | } 47 | 48 | /** 49 | * Match the eagerly loaded results to their parents. 50 | * 51 | * @param string $relation 52 | * @return array 53 | */ 54 | public function match(array $models, Collection $results, $relation) 55 | { 56 | return $this->matchMany($models, $results, $relation); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Model/CastsValue.php: -------------------------------------------------------------------------------- 1 | items = array_merge($this->items, $items); 27 | } 28 | 29 | public function __get($name) 30 | { 31 | return $this->items[$name] ?? null; 32 | } 33 | 34 | public function __set($name, $value) 35 | { 36 | $this->items[$name] = $value; 37 | $this->syncAttributes(); 38 | } 39 | 40 | public function __isset($name) 41 | { 42 | return isset($this->items[$name]); 43 | } 44 | 45 | public function __unset($name) 46 | { 47 | unset($this->items[$name]); 48 | $this->syncAttributes(); 49 | } 50 | 51 | public function isSynchronized(): bool 52 | { 53 | return $this->isSynchronized; 54 | } 55 | 56 | public function toArray(): array 57 | { 58 | return $this->items; 59 | } 60 | 61 | public function syncAttributes(): void 62 | { 63 | $this->isSynchronized = false; 64 | $this->model->syncAttributes(); 65 | $this->isSynchronized = true; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Model/Concerns/CamelCase.php: -------------------------------------------------------------------------------- 1 | toArray() as $key => $value) { 33 | $array[$this->keyTransform($key)] = $value; 34 | } 35 | return $array; 36 | } 37 | 38 | public function getFillable(): array 39 | { 40 | $fillable = []; 41 | foreach (parent::getFillable() as $key) { 42 | $fillable[] = $this->keyTransform($key); 43 | } 44 | return $fillable; 45 | } 46 | 47 | public function toArray(): array 48 | { 49 | $array = []; 50 | foreach (parent::toArray() as $key => $value) { 51 | $array[$this->keyTransform($key)] = $value; 52 | } 53 | return $array; 54 | } 55 | 56 | public function toOriginalArray(): array 57 | { 58 | return parent::toArray(); 59 | } 60 | 61 | protected function keyTransform($key) 62 | { 63 | return StrCache::camel($key); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Schema/ForeignIdColumnDefinition.php: -------------------------------------------------------------------------------- 1 | blueprint = $blueprint; 36 | } 37 | 38 | /** 39 | * Create a foreign key constraint on this column referencing the "id" column of the conventionally related table. 40 | * 41 | * @param null|string $table 42 | * @param string $column 43 | * @return ForeignKeyDefinition 44 | */ 45 | public function constrained($table = null, $column = 'id') 46 | { 47 | return $this->references($column)->on($table ?? Str::plural(Str::beforeLast($this->name, '_' . $column))); 48 | } 49 | 50 | /** 51 | * Specify which column this foreign ID references on another table. 52 | * 53 | * @param string $column 54 | * @return ForeignKeyDefinition 55 | */ 56 | public function references($column) 57 | { 58 | return $this->blueprint->foreign($this->name)->references($column); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Exception/QueryException.php: -------------------------------------------------------------------------------- 1 | code = $previous->getCode(); 34 | $this->message = $this->formatMessage($sql, $bindings, $previous); 35 | 36 | if ($previous instanceof PDOException) { 37 | $this->errorInfo = $previous->errorInfo; 38 | } 39 | } 40 | 41 | /** 42 | * Get the SQL for the query. 43 | */ 44 | public function getSql(): string 45 | { 46 | return $this->sql; 47 | } 48 | 49 | /** 50 | * Get the bindings for the query. 51 | */ 52 | public function getBindings(): array 53 | { 54 | return $this->bindings; 55 | } 56 | 57 | /** 58 | * Format the SQL error message. 59 | */ 60 | protected function formatMessage(string $sql, array $bindings, Throwable $previous): string 61 | { 62 | return $previous->getMessage() . ' (SQL: ' . build_sql($sql, $bindings) . ')'; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/database", 3 | "description": "A flexible database library.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "php", 7 | "hyperf", 8 | "database" 9 | ], 10 | "homepage": "https://hyperf.io", 11 | "support": { 12 | "issues": "https://github.com/hyperf/hyperf/issues", 13 | "source": "https://github.com/hyperf/hyperf", 14 | "docs": "https://hyperf.wiki", 15 | "pull-request": "https://github.com/hyperf/hyperf/pulls" 16 | }, 17 | "require": { 18 | "php": ">=8.1", 19 | "hyperf/code-parser": "~3.1.0", 20 | "hyperf/collection": "~3.1.23", 21 | "hyperf/conditionable": "~3.1.0", 22 | "hyperf/context": "~3.1.0", 23 | "hyperf/contract": "~3.1.0", 24 | "hyperf/engine": "^2.0", 25 | "hyperf/macroable": "~3.1.0", 26 | "hyperf/stringable": "~3.1.0", 27 | "hyperf/support": "~3.1.0", 28 | "hyperf/tappable": "~3.1.0", 29 | "nesbot/carbon": "^2.0", 30 | "psr/container": "^1.0 || ^2.0", 31 | "psr/event-dispatcher": "^1.0" 32 | }, 33 | "suggest": { 34 | "doctrine/dbal": "Required to rename columns (^3.0).", 35 | "hyperf/paginator": "Required to paginate the result set (~3.1.0).", 36 | "nikic/php-parser": "Required to use ModelCommand. (^4.0)", 37 | "php-di/phpdoc-reader": "Required to use ModelCommand. (^2.2)" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Hyperf\\Database\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "HyperfTest\\Database\\": "tests/" 47 | } 48 | }, 49 | "config": { 50 | "sort-packages": true 51 | }, 52 | "extra": { 53 | "branch-alias": { 54 | "dev-master": "3.1-dev" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Model/ModelNotFoundException.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | protected ?string $model = null; 28 | 29 | /** 30 | * The affected model IDs. 31 | * @var array 32 | */ 33 | protected array $ids = []; 34 | 35 | /** 36 | * Set the affected Model model and instance ids. 37 | * 38 | * @param array|int|string $ids 39 | * @return $this 40 | */ 41 | public function setModel(string $model, $ids = []) 42 | { 43 | $this->model = $model; 44 | $this->ids = Arr::wrap($ids); 45 | 46 | $this->message = "No query results for model [{$model}]"; 47 | 48 | if (count($this->ids) > 0) { 49 | $this->message .= ' ' . implode(', ', $this->ids); 50 | } else { 51 | $this->message .= '.'; 52 | } 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * Get the affected Model model. 59 | * 60 | * @return null|class-string 61 | */ 62 | public function getModel(): ?string 63 | { 64 | return $this->model; 65 | } 66 | 67 | /** 68 | * Get the affected Model model IDs. 69 | * 70 | * @return array 71 | */ 72 | public function getIds(): array 73 | { 74 | return $this->ids; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Model/Relations/Concerns/SupportsDefaultModels.php: -------------------------------------------------------------------------------- 1 | withDefault = $callback; 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * Make a new related instance for the given model. 44 | * 45 | * @return Model 46 | */ 47 | abstract protected function newRelatedInstanceFor(Model $parent); 48 | 49 | /** 50 | * Get the default value for this relation. 51 | * 52 | * @return null|Model 53 | */ 54 | protected function getDefaultFor(Model $parent) 55 | { 56 | if (! $this->withDefault) { 57 | return; 58 | } 59 | 60 | $instance = $this->newRelatedInstanceFor($parent); 61 | 62 | if (is_callable($this->withDefault)) { 63 | return call_user_func($this->withDefault, $instance, $parent) ?: $instance; 64 | } 65 | 66 | if (is_array($this->withDefault)) { 67 | $instance->forceFill($this->withDefault); 68 | } 69 | 70 | return $instance; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Commands/Migrations/BaseCommand.php: -------------------------------------------------------------------------------- 1 | input->hasOption('path') && $this->input->getOption('path')) { 30 | return collect($this->input->getOption('path'))->map(function ($path) { 31 | return ! $this->usingRealPath() 32 | ? BASE_PATH . DIRECTORY_SEPARATOR . $path 33 | : $path; 34 | })->all(); 35 | } 36 | 37 | return array_merge( 38 | $this->migrator->paths(), 39 | [$this->getMigrationPath()] 40 | ); 41 | } 42 | 43 | /** 44 | * Determine if the given path(s) are pre-resolved "real" paths. 45 | * 46 | * @return bool 47 | */ 48 | protected function usingRealPath() 49 | { 50 | return $this->input->hasOption('realpath') && $this->input->getOption('realpath'); 51 | } 52 | 53 | /** 54 | * Get the path to the migration directory. 55 | * 56 | * @return string 57 | */ 58 | protected function getMigrationPath() 59 | { 60 | return BASE_PATH . DIRECTORY_SEPARATOR . 'migrations'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Commands/Seeders/SeedCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Seed the database with records'); 30 | } 31 | 32 | /** 33 | * Handle the current command. 34 | */ 35 | public function handle() 36 | { 37 | if (! $this->confirmToProceed()) { 38 | return; 39 | } 40 | 41 | $this->seed->setOutput($this->output); 42 | 43 | if ($this->input->hasOption('database') && $this->input->getOption('database')) { 44 | $this->seed->setConnection($this->input->getOption('database')); 45 | } 46 | 47 | $this->seed->run($this->getSeederPaths()); 48 | } 49 | 50 | /** 51 | * Get the console command options. 52 | * 53 | * @return array 54 | */ 55 | protected function getOptions() 56 | { 57 | return [ 58 | ['path', null, InputOption::VALUE_OPTIONAL, 'The location where the seeders file stored'], 59 | ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided seeder file paths are pre-resolved absolute paths'], 60 | ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to seed'], 61 | ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], 62 | ]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Model/Relations/HasOne.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class HasOne extends HasOneOrMany 26 | { 27 | use SupportsDefaultModels; 28 | 29 | /** 30 | * Get the results of the relationship. 31 | */ 32 | public function getResults() 33 | { 34 | return $this->query->first() ?: $this->getDefaultFor($this->parent); 35 | } 36 | 37 | /** 38 | * Initialize the relation on a set of models. 39 | * 40 | * @param string $relation 41 | * @return array 42 | */ 43 | public function initRelation(array $models, $relation) 44 | { 45 | foreach ($models as $model) { 46 | $model->setRelation($relation, $this->getDefaultFor($model)); 47 | } 48 | 49 | return $models; 50 | } 51 | 52 | /** 53 | * Match the eagerly loaded results to their parents. 54 | * 55 | * @param string $relation 56 | * @return array 57 | */ 58 | public function match(array $models, Collection $results, $relation) 59 | { 60 | return $this->matchOne($models, $results, $relation); 61 | } 62 | 63 | /** 64 | * Make a new related instance for the given model. 65 | * 66 | * @return Model 67 | */ 68 | public function newRelatedInstanceFor(Model $parent) 69 | { 70 | return $this->related->newInstance()->setAttribute( 71 | $this->getForeignKeyName(), 72 | $parent->{$this->localKey} 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/ConnectionResolver.php: -------------------------------------------------------------------------------- 1 | $connection) { 33 | $this->addConnection($name, $connection); 34 | } 35 | } 36 | 37 | /** 38 | * Get a database connection instance. 39 | */ 40 | public function connection(?string $name = null): ConnectionInterface 41 | { 42 | if (is_null($name)) { 43 | $name = $this->getDefaultConnection(); 44 | } 45 | 46 | return $this->connections[$name]; 47 | } 48 | 49 | /** 50 | * Add a connection to the resolver. 51 | * 52 | * @param string $name 53 | */ 54 | public function addConnection($name, ConnectionInterface $connection) 55 | { 56 | $this->connections[$name] = $connection; 57 | } 58 | 59 | /** 60 | * Check if a connection has been registered. 61 | * 62 | * @param string $name 63 | */ 64 | public function hasConnection($name): bool 65 | { 66 | return isset($this->connections[$name]); 67 | } 68 | 69 | /** 70 | * Get the default connection name. 71 | */ 72 | public function getDefaultConnection(): string 73 | { 74 | return $this->default; 75 | } 76 | 77 | /** 78 | * Set the default connection name. 79 | */ 80 | public function setDefaultConnection(string $name): void 81 | { 82 | $this->default = $name; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Model/Relations/MorphOne.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class MorphOne extends MorphOneOrMany 26 | { 27 | use SupportsDefaultModels; 28 | 29 | /** 30 | * Get the results of the relationship. 31 | */ 32 | public function getResults() 33 | { 34 | return $this->query->first() ?: $this->getDefaultFor($this->parent); 35 | } 36 | 37 | /** 38 | * Initialize the relation on a set of models. 39 | * 40 | * @param string $relation 41 | * @return array 42 | */ 43 | public function initRelation(array $models, $relation) 44 | { 45 | foreach ($models as $model) { 46 | $model->setRelation($relation, $this->getDefaultFor($model)); 47 | } 48 | 49 | return $models; 50 | } 51 | 52 | /** 53 | * Match the eagerly loaded results to their parents. 54 | * 55 | * @param string $relation 56 | * @return array 57 | */ 58 | public function match(array $models, Collection $results, $relation) 59 | { 60 | return $this->matchOne($models, $results, $relation); 61 | } 62 | 63 | /** 64 | * Make a new related instance for the given model. 65 | * 66 | * @return TRelatedModel 67 | */ 68 | public function newRelatedInstanceFor(Model $parent) 69 | { 70 | return $this->related->newInstance() 71 | ->setAttribute($this->getForeignKeyName(), $parent->{$this->localKey}) 72 | ->setAttribute($this->getMorphType(), $this->morphClass); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Migrations/MigrationRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | getMessage(); 26 | 27 | return Str::contains($message, [ 28 | 'server has gone away', 29 | 'no connection to the server', 30 | 'Lost connection', 31 | 'is dead or not enabled', 32 | 'Error while sending', 33 | 'decryption failed or bad record mac', 34 | 'server closed the connection unexpectedly', 35 | 'SSL connection has been closed unexpectedly', 36 | 'Error writing data to the connection', 37 | 'Resource deadlock avoided', 38 | 'Transaction() on null', 39 | 'child connection forced to terminate due to client_idle_limit', 40 | 'query_wait_timeout', 41 | 'reset by peer', 42 | 'Physical connection is not usable', 43 | 'TCP Provider: Error code 0x68', 44 | 'Name or service not known', 45 | 'ORA-03114', 46 | 'Packets out of order. Expected', 47 | 'Broken pipe', 48 | 'Error reading result', 49 | // PDO::prepare(): Send of 77 bytes failed with errno=110 Operation timed out 50 | // SSL: Handshake timed out 51 | // SSL: Operation timed out 52 | // SSL: Connection timed out 53 | // SQLSTATE[HY000] [2002] Connection timed out 54 | 'timed out', 55 | // PDOStatement::execute(): Premature end of data 56 | 'Premature end of data', 57 | 'running with the --read-only option so it cannot execute this statement', 58 | ]); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Schema/ColumnDefinition.php: -------------------------------------------------------------------------------- 1 | setDescription('Create a new seeder class'); 29 | } 30 | 31 | /** 32 | * Handle the current command. 33 | */ 34 | public function handle() 35 | { 36 | $name = Str::snake(trim($this->input->getArgument('name'))); 37 | 38 | $this->writeMigration($name); 39 | } 40 | 41 | /** 42 | * Write the seeder file to disk. 43 | */ 44 | protected function writeMigration(string $name) 45 | { 46 | $path = $this->ensureSeederDirectoryAlreadyExist( 47 | $this->getSeederPath() 48 | ); 49 | 50 | $file = pathinfo($this->creator->create($name, $path), PATHINFO_FILENAME); 51 | 52 | $this->info("[INFO] Created Seeder: {$file}"); 53 | } 54 | 55 | protected function ensureSeederDirectoryAlreadyExist(string $path) 56 | { 57 | if (! file_exists($path)) { 58 | mkdir($path, 0755, true); 59 | } 60 | 61 | return $path; 62 | } 63 | 64 | protected function getArguments(): array 65 | { 66 | return [ 67 | ['name', InputArgument::REQUIRED, 'The name of the seeder'], 68 | ]; 69 | } 70 | 71 | protected function getOptions(): array 72 | { 73 | return [ 74 | ['path', null, InputOption::VALUE_OPTIONAL, 'The location where the seeder file should be created'], 75 | ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided seeder file paths are pre-resolved absolute paths'], 76 | ]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Model/Register.php: -------------------------------------------------------------------------------- 1 | connection($connection); 39 | } 40 | 41 | /** 42 | * Get the connection resolver instance. 43 | */ 44 | public static function getConnectionResolver(): ?ConnectionResolverInterface 45 | { 46 | return static::$resolver; 47 | } 48 | 49 | /** 50 | * Set the connection resolver instance. 51 | */ 52 | public static function setConnectionResolver(ConnectionResolverInterface $resolver) 53 | { 54 | static::$resolver = $resolver; 55 | } 56 | 57 | /** 58 | * Unset the connection resolver for models. 59 | */ 60 | public static function unsetConnectionResolver(): void 61 | { 62 | static::$resolver = null; 63 | } 64 | 65 | /** 66 | * Get the event dispatcher instance. 67 | */ 68 | public static function getEventDispatcher(): ?EventDispatcherInterface 69 | { 70 | return static::$dispatcher; 71 | } 72 | 73 | /** 74 | * Set the event dispatcher instance. 75 | */ 76 | public static function setEventDispatcher(EventDispatcherInterface $dispatcher) 77 | { 78 | static::$dispatcher = $dispatcher; 79 | } 80 | 81 | /** 82 | * Unset the event dispatcher for models. 83 | */ 84 | public static function unsetEventDispatcher(): void 85 | { 86 | static::$dispatcher = null; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Model/Concerns/HasGlobalScopes.php: -------------------------------------------------------------------------------- 1 | column_name; 26 | }, $results); 27 | } 28 | 29 | public function processColumns(array $results): array 30 | { 31 | $columns = []; 32 | foreach ($results as $i => $value) { 33 | $item = array_change_key_case((array) $value, CASE_LOWER); 34 | $columns[$i] = new Column( 35 | $item['table_schema'], 36 | $item['table_name'], 37 | $item['column_name'], 38 | $item['ordinal_position'], 39 | $item['column_default'], 40 | $item['is_nullable'] === 'YES', 41 | $item['data_type'], 42 | $item['column_comment'] 43 | ); 44 | } 45 | 46 | return $columns; 47 | } 48 | 49 | /** 50 | * Process the results of a column type listing query. 51 | */ 52 | public function processListing(array $results): array 53 | { 54 | return array_map(function ($result) { 55 | return (array) $result; 56 | }, $results); 57 | } 58 | 59 | /** 60 | * Process the results of a foreign keys query. 61 | */ 62 | public function processForeignKeys(array $results): array 63 | { 64 | return array_map(function ($result) { 65 | $result = (object) $result; 66 | 67 | return [ 68 | 'name' => $result->name, 69 | 'columns' => explode(',', $result->columns), 70 | 'foreign_schema' => $result->foreign_schema, 71 | 'foreign_table' => $result->foreign_table, 72 | 'foreign_columns' => explode(',', $result->foreign_columns), 73 | 'on_update' => strtolower($result->on_update), 74 | 'on_delete' => strtolower($result->on_delete), 75 | ]; 76 | }, $results); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Commands/Migrations/ResetCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Rollback all database migrations'); 30 | } 31 | 32 | /** 33 | * Execute the console command. 34 | */ 35 | public function handle() 36 | { 37 | if (! $this->confirmToProceed()) { 38 | return; 39 | } 40 | 41 | $this->migrator->setConnection($this->input->getOption('database') ?? 'default'); 42 | 43 | // First, we'll make sure that the migration table actually exists before we 44 | // start trying to rollback and re-run all of the migrations. If it's not 45 | // present we'll just bail out with an info message for the developers. 46 | if (! $this->migrator->repositoryExists()) { 47 | return $this->comment('Migration table not found.'); 48 | } 49 | 50 | $this->migrator->setOutput($this->output)->reset( 51 | $this->getMigrationPaths(), 52 | $this->input->getOption('pretend') 53 | ); 54 | } 55 | 56 | /** 57 | * Get the console command options. 58 | * 59 | * @return array 60 | */ 61 | protected function getOptions() 62 | { 63 | return [ 64 | ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], 65 | ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], 66 | ['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'], 67 | ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], 68 | ['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run'], 69 | ]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Commands/Migrations/RollbackCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Rollback the last database migration'); 30 | } 31 | 32 | /** 33 | * Execute the console command. 34 | */ 35 | public function handle() 36 | { 37 | if (! $this->confirmToProceed()) { 38 | return; 39 | } 40 | 41 | $this->migrator->setConnection($this->input->getOption('database') ?? 'default'); 42 | 43 | $this->migrator->setOutput($this->output)->rollback( 44 | $this->getMigrationPaths(), 45 | [ 46 | 'pretend' => $this->input->getOption('pretend'), 47 | 'step' => (int) $this->input->getOption('step'), 48 | 'batch' => (int) $this->input->getOption('batch'), 49 | ] 50 | ); 51 | } 52 | 53 | /** 54 | * Get the console command options. 55 | * 56 | * @return array 57 | */ 58 | protected function getOptions() 59 | { 60 | return [ 61 | ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], 62 | ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], 63 | ['path', null, InputOption::VALUE_OPTIONAL, 'The path to the migrations files to be executed'], 64 | ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], 65 | ['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run'], 66 | ['step', null, InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted'], 67 | ['batch', null, InputOption::VALUE_OPTIONAL, 'The batch of migrations (identified by their batch number) to be reverted'], 68 | ]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Schema/Grammars/RenameColumn.php: -------------------------------------------------------------------------------- 1 | getDoctrineColumn( 30 | $grammar->getTablePrefix() . $blueprint->getTable(), 31 | $command->from 32 | ); 33 | 34 | $schema = $connection->getDoctrineSchemaManager(); 35 | 36 | return (array) $schema->getDatabasePlatform()->getAlterTableSQL(static::getRenamedDiff( 37 | $grammar, 38 | $blueprint, 39 | $command, 40 | $column, 41 | $schema 42 | )); 43 | } 44 | 45 | /** 46 | * Get a new column instance with the new column name. 47 | * 48 | * @return TableDiff 49 | */ 50 | protected static function getRenamedDiff(Grammar $grammar, Blueprint $blueprint, Fluent $command, Column $column, SchemaManager $schema) 51 | { 52 | return static::setRenamedColumns( 53 | $grammar->getDoctrineTableDiff($blueprint, $schema), 54 | $command, 55 | $column 56 | ); 57 | } 58 | 59 | /** 60 | * Set the renamed columns on the table diff. 61 | * 62 | * @return TableDiff 63 | */ 64 | protected static function setRenamedColumns(TableDiff $tableDiff, Fluent $command, Column $column) 65 | { 66 | $tableDiff->renamedColumns = [ 67 | $command->from => new Column($command->to, $column->getType(), self::getWritableColumnOptions($column)), 68 | ]; 69 | return $tableDiff; 70 | } 71 | 72 | /** 73 | * Get the writable column options. 74 | * 75 | * @return array 76 | */ 77 | private static function getWritableColumnOptions(Column $column) 78 | { 79 | return array_filter($column->toArray(), function (string $name) use ($column) { 80 | return method_exists($column, 'set' . $name); 81 | }, ARRAY_FILTER_USE_KEY); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/MySqlConnection.php: -------------------------------------------------------------------------------- 1 | schemaGrammar)) { 32 | $this->useDefaultSchemaGrammar(); 33 | } 34 | 35 | return new MySqlBuilder($this); 36 | } 37 | 38 | /** 39 | * Bind values to their parameters in the given statement. 40 | */ 41 | public function bindValues(PDOStatement $statement, array $bindings): void 42 | { 43 | foreach ($bindings as $key => $value) { 44 | $statement->bindValue( 45 | is_string($key) ? $key : $key + 1, 46 | $value 47 | ); 48 | } 49 | } 50 | 51 | /** 52 | * Determine if the given database exception was caused by a unique constraint violation. 53 | * 54 | * @return bool 55 | */ 56 | protected function isUniqueConstraintError(Exception $exception) 57 | { 58 | return boolval(preg_match('#Integrity constraint violation: 1062#i', $exception->getMessage())); 59 | } 60 | 61 | /** 62 | * Get the default query grammar instance. 63 | */ 64 | protected function getDefaultQueryGrammar(): QueryGrammar 65 | { 66 | return $this->withTablePrefix(new QueryGrammar()); 67 | } 68 | 69 | /** 70 | * Get the default schema grammar instance. 71 | */ 72 | protected function getDefaultSchemaGrammar(): SchemaGrammar 73 | { 74 | return $this->withTablePrefix(new SchemaGrammar()); 75 | } 76 | 77 | /** 78 | * Get the default post processor instance. 79 | */ 80 | protected function getDefaultPostProcessor(): MySqlProcessor 81 | { 82 | return new MySqlProcessor(); 83 | } 84 | 85 | /** 86 | * Get the Doctrine DBAL driver. 87 | * 88 | * @return Driver 89 | */ 90 | protected function getDoctrineDriver() 91 | { 92 | return new MySqlDriver(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Model/Relations/HasOneThrough.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class HasOneThrough extends HasManyThrough 27 | { 28 | use SupportsDefaultModels; 29 | 30 | /** 31 | * Get the results of the relationship. 32 | * 33 | * @return null|TRelatedModel 34 | */ 35 | public function getResults() 36 | { 37 | return $this->first() ?: $this->getDefaultFor($this->farParent); 38 | } 39 | 40 | /** 41 | * Initialize the relation on a set of models. 42 | * 43 | * @param string $relation 44 | * @return array 45 | */ 46 | public function initRelation(array $models, $relation) 47 | { 48 | foreach ($models as $model) { 49 | $model->setRelation($relation, $this->getDefaultFor($model)); 50 | } 51 | 52 | return $models; 53 | } 54 | 55 | /** 56 | * Match the eagerly loaded results to their parents. 57 | * 58 | * @param string $relation 59 | * @return array 60 | */ 61 | public function match(array $models, Collection $results, $relation) 62 | { 63 | $dictionary = $this->buildDictionary($results); 64 | 65 | // Once we have the dictionary we can simply spin through the parent models to 66 | // link them up with their children using the keyed dictionary to make the 67 | // matching very convenient and easy work. Then we'll just return them. 68 | foreach ($models as $model) { 69 | if (isset($dictionary[$key = $model->getAttribute($this->localKey)])) { 70 | $value = $dictionary[$key]; 71 | $model->setRelation( 72 | $relation, 73 | reset($value) 74 | ); 75 | } 76 | } 77 | 78 | return $models; 79 | } 80 | 81 | /** 82 | * Make a new related instance for the given model. 83 | * 84 | * @return TRelatedModel 85 | */ 86 | public function newRelatedInstanceFor(Model $parent) 87 | { 88 | return $this->related->newInstance(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Commands/Ast/ModelRewriteConnectionVisitor.php: -------------------------------------------------------------------------------- 1 | props[0]->name->toLowerString() === 'connection') { 34 | $this->hasConnection = true; 35 | 36 | if ($this->shouldRemovedConnection()) { 37 | return NodeTraverser::REMOVE_NODE; 38 | } 39 | 40 | $node->props[0]->default = new Node\Scalar\String_($this->connection); 41 | $node->type = new Node\NullableType(new Identifier('string')); 42 | } 43 | 44 | return $node; 45 | } 46 | 47 | return null; 48 | } 49 | 50 | public function afterTraverse(array $nodes) 51 | { 52 | if ($this->hasConnection || $this->shouldRemovedConnection()) { 53 | return null; 54 | } 55 | 56 | foreach ($nodes as $namespace) { 57 | if (! $namespace instanceof Node\Stmt\Namespace_) { 58 | continue; 59 | } 60 | foreach ($namespace->stmts as $class) { 61 | if (! $class instanceof Node\Stmt\Class_) { 62 | continue; 63 | } 64 | foreach ($class->stmts as $property) { 65 | $flags = Node\Stmt\Class_::MODIFIER_PROTECTED; 66 | $prop = new Node\Stmt\PropertyProperty('connection', new Node\Scalar\String_($this->connection)); 67 | $class->stmts[] = new Node\Stmt\Property($flags, [$prop], type: new Node\NullableType(new Identifier('string'))); 68 | return null; 69 | } 70 | } 71 | } 72 | 73 | return null; 74 | } 75 | 76 | protected function shouldRemovedConnection(): bool 77 | { 78 | $ref = new ReflectionClass($this->class); 79 | 80 | if (! $ref->getParentClass()) { 81 | return false; 82 | } 83 | 84 | $connection = $ref->getParentClass()->getDefaultProperties()['connection'] ?? null; 85 | return $connection === $this->connection; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Commands/Ast/ModelRewriteInheritanceVisitor.php: -------------------------------------------------------------------------------- 1 | getUses())) { 31 | preg_match_all('/\s*([a-z0-9\\\]+)(as)?([a-z0-9]+)?;?\s*/is', $option->getUses(), $match); 32 | if (isset($match[1][0])) { 33 | $this->parentClass = $match[1][0]; 34 | } 35 | } 36 | } 37 | 38 | public function afterTraverse(array $nodes) 39 | { 40 | if (empty($this->option->getUses())) { 41 | return null; 42 | } 43 | 44 | $use = new Node\Stmt\UseUse( 45 | new Node\Name($this->parentClass), 46 | $this->option->getInheritance() 47 | ); 48 | 49 | foreach ($nodes as $namespace) { 50 | if (! $namespace instanceof Node\Stmt\Namespace_) { 51 | continue; 52 | } 53 | 54 | if ($this->shouldAddUseUse) { 55 | array_unshift($namespace->stmts, new Node\Stmt\Use_([$use])); 56 | } 57 | } 58 | 59 | return null; 60 | } 61 | 62 | public function leaveNode(Node $node) 63 | { 64 | switch ($node) { 65 | case $node instanceof Node\Stmt\Class_: 66 | $inheritance = $this->option->getInheritance(); 67 | if (is_object($node->extends) && ! empty($inheritance)) { 68 | $node->extends->parts = [$inheritance]; 69 | } 70 | return $node; 71 | case $node instanceof Node\Stmt\UseUse: 72 | $class = implode('\\', $node->name->parts); 73 | $alias = is_object($node->alias) ? $node->alias->name : null; 74 | if ($class == $this->parentClass) { 75 | // The parent class is exists. 76 | $this->shouldAddUseUse = false; 77 | if (end($node->name->parts) !== $this->option->getInheritance() && $alias !== $this->option->getInheritance()) { 78 | // Rewrite the alias, if the class is not equal with inheritance. 79 | $node->alias = new Identifier($this->option->getInheritance()); 80 | } 81 | } 82 | return $node; 83 | } 84 | 85 | return null; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Model/Concerns/HasTimestamps.php: -------------------------------------------------------------------------------- 1 | usesTimestamps()) { 31 | return false; 32 | } 33 | 34 | $this->updateTimestamps(); 35 | 36 | return $this->save(); 37 | } 38 | 39 | /** 40 | * Set the value of the "created at" attribute. 41 | * 42 | * @param mixed $value 43 | */ 44 | public function setCreatedAt($value): static 45 | { 46 | $this->{static::CREATED_AT} = $value; 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * Set the value of the "updated at" attribute. 53 | * 54 | * @param mixed $value 55 | */ 56 | public function setUpdatedAt($value): static 57 | { 58 | $this->{static::UPDATED_AT} = $value; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Get a fresh timestamp for the model. 65 | */ 66 | public function freshTimestamp(): CarbonInterface 67 | { 68 | return Carbon::now(); 69 | } 70 | 71 | /** 72 | * Get a fresh timestamp for the model. 73 | */ 74 | public function freshTimestampString(): ?string 75 | { 76 | return $this->fromDateTime($this->freshTimestamp()); 77 | } 78 | 79 | /** 80 | * Determine if the model uses timestamps. 81 | */ 82 | public function usesTimestamps(): bool 83 | { 84 | return $this->timestamps; 85 | } 86 | 87 | /** 88 | * Get the name of the "created at" column. 89 | */ 90 | public function getCreatedAtColumn(): ?string 91 | { 92 | return static::CREATED_AT; 93 | } 94 | 95 | /** 96 | * Get the name of the "updated at" column. 97 | */ 98 | public function getUpdatedAtColumn(): ?string 99 | { 100 | return static::UPDATED_AT; 101 | } 102 | 103 | /** 104 | * Update the creation and update timestamps. 105 | */ 106 | protected function updateTimestamps(): void 107 | { 108 | $time = $this->freshTimestamp(); 109 | 110 | if (! is_null(static::UPDATED_AT) && ! $this->isDirty(static::UPDATED_AT)) { 111 | $this->setUpdatedAt($time); 112 | } 113 | 114 | if (! $this->exists && ! is_null(static::CREATED_AT) 115 | && ! $this->isDirty(static::CREATED_AT)) { 116 | $this->setCreatedAt($time); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Commands/Migrations/StatusCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Show the status of each migration'); 28 | } 29 | 30 | /** 31 | * Execute the console command. 32 | */ 33 | public function handle() 34 | { 35 | $this->migrator->setConnection($this->input->getOption('database') ?? 'default'); 36 | 37 | if (! $this->migrator->repositoryExists()) { 38 | return $this->error('Migration table not found.'); 39 | } 40 | 41 | $ran = $this->migrator->getRepository()->getRan(); 42 | 43 | $batches = $this->migrator->getRepository()->getMigrationBatches(); 44 | 45 | if (count($migrations = $this->getStatusFor($ran, $batches)) > 0) { 46 | $this->table(['Ran?', 'Migration', 'Batch'], $migrations); 47 | } else { 48 | $this->error('No migrations found'); 49 | } 50 | } 51 | 52 | /** 53 | * Get the status for the given ran migrations. 54 | * 55 | * @return Collection 56 | */ 57 | protected function getStatusFor(array $ran, array $batches) 58 | { 59 | return Collection::make($this->getAllMigrationFiles()) 60 | ->map(function ($migration) use ($ran, $batches) { 61 | $migrationName = $this->migrator->getMigrationName($migration); 62 | 63 | return in_array($migrationName, $ran) 64 | ? ['Yes', $migrationName, $batches[$migrationName]] 65 | : ['No', $migrationName]; 66 | }); 67 | } 68 | 69 | /** 70 | * Get an array of all of the migration files. 71 | * 72 | * @return array 73 | */ 74 | protected function getAllMigrationFiles() 75 | { 76 | return $this->migrator->getMigrationFiles($this->getMigrationPaths()); 77 | } 78 | 79 | /** 80 | * Get the console command options. 81 | * 82 | * @return array 83 | */ 84 | protected function getOptions() 85 | { 86 | return [ 87 | ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], 88 | ['path', null, InputOption::VALUE_OPTIONAL, 'The path to the migrations files to use'], 89 | ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], 90 | ]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Schema/Schema.php: -------------------------------------------------------------------------------- 1 | get(ConnectionResolverInterface::class); 58 | $connection = $resolver->connection(); 59 | return $connection->getSchemaBuilder()->{$name}(...$arguments); 60 | } 61 | 62 | public function __call($name, $arguments) 63 | { 64 | return self::__callStatic($name, $arguments); 65 | } 66 | 67 | /** 68 | * Create a connection by ConnectionResolver. 69 | */ 70 | public function connection(string $name = 'default'): ConnectionInterface 71 | { 72 | $container = ApplicationContext::getContainer(); 73 | $resolver = $container->get(ConnectionResolverInterface::class); 74 | return $resolver->connection($name); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Seeders/SeederCreator.php: -------------------------------------------------------------------------------- 1 | files = $files; 34 | } 35 | 36 | /** 37 | * Create a new seeder at the given path. 38 | * 39 | * @param string $name 40 | * @param string $path 41 | * @return string 42 | */ 43 | public function create($name, $path) 44 | { 45 | $this->ensureSeederDoesntAlreadyExist($name); 46 | 47 | $stub = $this->getStub(); 48 | 49 | $this->files->put( 50 | $path = $this->getPath($name, $path), 51 | $this->populateStub($name, $stub) 52 | ); 53 | 54 | return $path; 55 | } 56 | 57 | /** 58 | * Get the path to the stubs. 59 | * 60 | * @return string 61 | */ 62 | public function stubPath() 63 | { 64 | return __DIR__ . '/stubs'; 65 | } 66 | 67 | /** 68 | * Get the filesystem instance. 69 | * 70 | * @return Filesystem 71 | */ 72 | public function getFilesystem() 73 | { 74 | return $this->files; 75 | } 76 | 77 | /** 78 | * Get the seeder stub file. 79 | * 80 | * @return string 81 | */ 82 | protected function getStub() 83 | { 84 | return $this->files->get($this->stubPath() . '/seeder.stub'); 85 | } 86 | 87 | /** 88 | * Populate the place-holders in the seeder stub. 89 | * 90 | * @param string $name 91 | * @param string $stub 92 | * @return string 93 | */ 94 | protected function populateStub($name, $stub) 95 | { 96 | return str_replace('DummyClass', $this->getClassName($name), $stub); 97 | } 98 | 99 | /** 100 | * Ensure that a seeder with the given name doesn't already exist. 101 | * 102 | * @param string $name 103 | * 104 | * @throws InvalidArgumentException 105 | */ 106 | protected function ensureSeederDoesntAlreadyExist($name) 107 | { 108 | if (class_exists($className = $this->getClassName($name))) { 109 | throw new InvalidArgumentException("A {$className} class already exists."); 110 | } 111 | } 112 | 113 | /** 114 | * Get the class name of a seeder name. 115 | * 116 | * @param string $name 117 | * @return string 118 | */ 119 | protected function getClassName($name) 120 | { 121 | return Str::studly($name); 122 | } 123 | 124 | /** 125 | * Get the full path to the seeder. 126 | * 127 | * @param string $name 128 | * @param string $path 129 | * @return string 130 | */ 131 | protected function getPath($name, $path) 132 | { 133 | return $path . '/' . $name . '.php'; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/ConnectionInterface.php: -------------------------------------------------------------------------------- 1 | getConnection()->insert($sql, $values); 42 | 43 | $id = $query->getConnection()->getPdo()->lastInsertId($sequence); 44 | 45 | return is_numeric($id) ? (int) $id : $id; 46 | } 47 | 48 | /** 49 | * Process the results of a tables query. 50 | */ 51 | public function processTables(array $results): array 52 | { 53 | return array_map(static function ($result) { 54 | $result = (object) $result; 55 | 56 | return [ 57 | 'name' => $result->name, 58 | 'schema' => $result->schema ?? null, // PostgreSQL and SQL Server 59 | 'size' => isset($result->size) ? (int) $result->size : null, 60 | 'comment' => $result->comment ?? null, // MySQL and PostgreSQL 61 | 'collation' => $result->collation ?? null, // MySQL only 62 | 'engine' => $result->engine ?? null, // MySQL only 63 | ]; 64 | }, $results); 65 | } 66 | 67 | /** 68 | * Process the results of a column listing query. 69 | */ 70 | public function processColumnListing(array $results): array 71 | { 72 | return $results; 73 | } 74 | 75 | /** 76 | * @return Column[] 77 | */ 78 | public function processColumns(array $results): array 79 | { 80 | $columns = []; 81 | foreach ($results as $item) { 82 | $columns[] = new Column(...array_values($item)); 83 | } 84 | 85 | return $columns; 86 | } 87 | 88 | /** 89 | * Process the results of an indexes query. 90 | */ 91 | public function processIndexes(array $results): array 92 | { 93 | return $results; 94 | } 95 | 96 | /** 97 | * Process the results of a views query. 98 | */ 99 | public function processViews(array $results): array 100 | { 101 | return array_map(function ($result) { 102 | $result = (object) $result; 103 | 104 | return [ 105 | 'name' => $result->name, 106 | 'schema' => $result->schema ?? null, // PostgreSQL and SQL Server 107 | 'definition' => $result->definition, 108 | ]; 109 | }, $results); 110 | } 111 | 112 | /** 113 | * Process the results of a foreign keys query. 114 | */ 115 | public function processForeignKeys(array $results): array 116 | { 117 | return $results; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Model/Relations/MorphOneOrMany.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | abstract class MorphOneOrMany extends HasOneOrMany 28 | { 29 | /** 30 | * The foreign key type for the relationship. 31 | * 32 | * @var string 33 | */ 34 | protected $morphType; 35 | 36 | /** 37 | * The class name of the parent model. 38 | * 39 | * @var string 40 | */ 41 | protected $morphClass; 42 | 43 | /** 44 | * Create a new morph one or many relationship instance. 45 | * 46 | * @param string $type 47 | * @param string $id 48 | * @param string $localKey 49 | */ 50 | public function __construct(Builder $query, Model $parent, $type, $id, $localKey) 51 | { 52 | $this->morphType = $type; 53 | 54 | $this->morphClass = $parent->getMorphClass(); 55 | 56 | parent::__construct($query, $parent, $id, $localKey); 57 | } 58 | 59 | /** 60 | * Set the base constraints on the relation query. 61 | */ 62 | public function addConstraints() 63 | { 64 | if (Constraint::isConstraint()) { 65 | parent::addConstraints(); 66 | 67 | $this->query->where($this->morphType, $this->morphClass); 68 | } 69 | } 70 | 71 | /** 72 | * Set the constraints for an eager load of the relation. 73 | */ 74 | public function addEagerConstraints(array $models) 75 | { 76 | parent::addEagerConstraints($models); 77 | 78 | $this->query->where($this->morphType, $this->morphClass); 79 | } 80 | 81 | /** 82 | * Get the relationship query. 83 | * 84 | * @param array|mixed $columns 85 | * @return Builder 86 | */ 87 | public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) 88 | { 89 | return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where( 90 | $this->morphType, 91 | $this->morphClass 92 | ); 93 | } 94 | 95 | /** 96 | * Get the foreign key "type" name. 97 | * 98 | * @return string 99 | */ 100 | public function getQualifiedMorphType() 101 | { 102 | return $this->morphType; 103 | } 104 | 105 | /** 106 | * Get the plain morph type name without the table. 107 | * 108 | * @return string 109 | */ 110 | public function getMorphType() 111 | { 112 | return last(explode('.', $this->morphType)); 113 | } 114 | 115 | /** 116 | * Get the class name of the parent model. 117 | * 118 | * @return string 119 | */ 120 | public function getMorphClass() 121 | { 122 | return $this->morphClass; 123 | } 124 | 125 | /** 126 | * Set the foreign ID and type for creating a related model. 127 | */ 128 | protected function setForeignAttributesForCreate(Model $model) 129 | { 130 | $model->{$this->getForeignKeyName()} = $this->getParentKey(); 131 | 132 | $model->{$this->getMorphType()} = $this->morphClass; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Model/Relations/MorphPivot.php: -------------------------------------------------------------------------------- 1 | getDeleteQuery(); 46 | 47 | $query->where($this->morphType, $this->morphClass); 48 | 49 | return $query->delete(); 50 | } 51 | 52 | /** 53 | * Set the morph type for the pivot. 54 | * 55 | * @param string $morphType 56 | * @return $this 57 | */ 58 | public function setMorphType($morphType) 59 | { 60 | $this->morphType = $morphType; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Set the morph class for the pivot. 67 | * 68 | * @param string $morphClass 69 | * @return MorphPivot 70 | */ 71 | public function setMorphClass($morphClass) 72 | { 73 | $this->morphClass = $morphClass; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Get a new query to restore one or more models by their queueable IDs. 80 | * 81 | * @param array|int $ids 82 | * @return Builder 83 | */ 84 | public function newQueryForRestoration($ids) 85 | { 86 | if (is_array($ids)) { 87 | return $this->newQueryForCollectionRestoration($ids); 88 | } 89 | 90 | if (! Str::contains($ids, ':')) { 91 | return parent::newQueryForRestoration($ids); 92 | } 93 | 94 | $segments = explode(':', $ids); 95 | 96 | return $this->newQueryWithoutScopes() 97 | ->where($segments[0], $segments[1]) 98 | ->where($segments[2], $segments[3]) 99 | ->where($segments[4], $segments[5]); 100 | } 101 | 102 | /** 103 | * Set the keys for a save update query. 104 | * 105 | * @return Builder 106 | */ 107 | protected function setKeysForSaveQuery(Builder $query) 108 | { 109 | $query->where($this->morphType, $this->morphClass); 110 | 111 | return parent::setKeysForSaveQuery($query); 112 | } 113 | 114 | /** 115 | * Get a new query to restore multiple models by their queueable IDs. 116 | * 117 | * @param array $ids 118 | * @return Builder 119 | */ 120 | protected function newQueryForCollectionRestoration(array $ids) 121 | { 122 | if (! Str::contains($ids[0], ':')) { 123 | return parent::newQueryForRestoration($ids); 124 | } 125 | 126 | $query = $this->newQueryWithoutScopes(); 127 | 128 | foreach ($ids as $id) { 129 | $segments = explode(':', $id); 130 | 131 | $query->orWhere(function ($query) use ($segments) { 132 | return $query->where($segments[0], $segments[1]) 133 | ->where($segments[2], $segments[3]) 134 | ->where($segments[4], $segments[5]); 135 | }); 136 | } 137 | 138 | return $query; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Connectors/Connector.php: -------------------------------------------------------------------------------- 1 | PDO::CASE_NATURAL, 31 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 32 | PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, 33 | PDO::ATTR_STRINGIFY_FETCHES => false, 34 | PDO::ATTR_EMULATE_PREPARES => false, 35 | ]; 36 | 37 | /** 38 | * Create a new PDO connection. 39 | * 40 | * @param string $dsn 41 | * @return PDO 42 | * @throws Exception 43 | */ 44 | public function createConnection($dsn, array $config, array $options) 45 | { 46 | [$username, $password] = [ 47 | $config['username'] ?? null, $config['password'] ?? null, 48 | ]; 49 | 50 | try { 51 | return $this->createPdoConnection( 52 | $dsn, 53 | $username, 54 | $password, 55 | $options 56 | ); 57 | } catch (Exception $e) { 58 | return $this->tryAgainIfCausedByLostConnection( 59 | $e, 60 | $dsn, 61 | $username, 62 | $password, 63 | $options 64 | ); 65 | } 66 | } 67 | 68 | /** 69 | * Get the PDO options based on the configuration. 70 | * 71 | * @return array 72 | */ 73 | public function getOptions(array $config) 74 | { 75 | return array_replace($this->options, $config['options'] ?? []); 76 | } 77 | 78 | /** 79 | * Get the default PDO connection options. 80 | * 81 | * @return array 82 | */ 83 | public function getDefaultOptions() 84 | { 85 | return $this->options; 86 | } 87 | 88 | /** 89 | * Set the default PDO connection options. 90 | */ 91 | public function setDefaultOptions(array $options) 92 | { 93 | $this->options = $options; 94 | } 95 | 96 | /** 97 | * Create a new PDO connection instance. 98 | * 99 | * @param string $dsn 100 | * @param string $username 101 | * @param string $password 102 | * @param array $options 103 | * @return PDO 104 | */ 105 | protected function createPdoConnection($dsn, $username, $password, $options) 106 | { 107 | return new PDO($dsn, $username, $password, $options); 108 | } 109 | 110 | /** 111 | * Determine if the connection is persistent. 112 | * 113 | * @param array $options 114 | * @return bool 115 | */ 116 | protected function isPersistentConnection($options) 117 | { 118 | return isset($options[PDO::ATTR_PERSISTENT]) 119 | && $options[PDO::ATTR_PERSISTENT]; 120 | } 121 | 122 | /** 123 | * Handle an exception that occurred during connect execution. 124 | * 125 | * @param string $dsn 126 | * @param string $username 127 | * @param string $password 128 | * @param array $options 129 | * @return PDO 130 | * @throws Exception 131 | */ 132 | protected function tryAgainIfCausedByLostConnection(Throwable $e, $dsn, $username, $password, $options) 133 | { 134 | if ($this->causedByLostConnection($e)) { 135 | return $this->createPdoConnection($dsn, $username, $password, $options); 136 | } 137 | 138 | throw $e; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Model/Concerns/HidesAttributes.php: -------------------------------------------------------------------------------- 1 | hidden; 37 | } 38 | 39 | /** 40 | * Set the hidden attributes for the model. 41 | */ 42 | public function setHidden(array $hidden): static 43 | { 44 | $this->hidden = $hidden; 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * Add hidden attributes for the model. 51 | * 52 | * @param null|array|string $attributes 53 | */ 54 | public function addHidden($attributes = null): void 55 | { 56 | $this->hidden = array_merge( 57 | $this->hidden, 58 | is_array($attributes) ? $attributes : func_get_args() 59 | ); 60 | } 61 | 62 | /** 63 | * Get the visible attributes for the model. 64 | */ 65 | public function getVisible(): array 66 | { 67 | return $this->visible; 68 | } 69 | 70 | /** 71 | * Set the visible attributes for the model. 72 | */ 73 | public function setVisible(array $visible): static 74 | { 75 | $this->visible = $visible; 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Add visible attributes for the model. 82 | * 83 | * @param null|array|string $attributes 84 | */ 85 | public function addVisible($attributes = null): void 86 | { 87 | $this->visible = array_merge( 88 | $this->visible, 89 | is_array($attributes) ? $attributes : func_get_args() 90 | ); 91 | } 92 | 93 | /** 94 | * Make the given, typically hidden, attributes visible. 95 | * 96 | * @param array|string $attributes 97 | */ 98 | public function makeVisible($attributes): static 99 | { 100 | $this->hidden = array_diff($this->hidden, (array) $attributes); 101 | 102 | if (! empty($this->visible)) { 103 | $this->addVisible($attributes); 104 | } 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Make the given, typically visible, attributes hidden. 111 | * 112 | * @param array|string $attributes 113 | */ 114 | public function makeHidden($attributes): static 115 | { 116 | $attributes = (array) $attributes; 117 | 118 | $this->visible = array_diff($this->visible, $attributes); 119 | 120 | $this->hidden = array_unique(array_merge($this->hidden, $attributes)); 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * Make the given, typically visible, attributes hidden if the given truth test passes. 127 | */ 128 | public function makeHiddenIf(bool|Closure $condition, null|array|string $attributes): static 129 | { 130 | return value($condition, $this) ? $this->makeHidden($attributes) : $this; 131 | } 132 | 133 | /** 134 | * Make the given, typically hidden, attributes visible if the given truth test passes. 135 | */ 136 | public function makeVisibleIf(bool|Closure $condition, null|array|string $attributes): static 137 | { 138 | return value($condition, $this) ? $this->makeVisible($attributes) : $this; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Commands/Migrations/MigrateCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Run the database migrations'); 31 | } 32 | 33 | /** 34 | * Execute the console command. 35 | */ 36 | public function handle() 37 | { 38 | if (! $this->confirmToProceed()) { 39 | return 0; 40 | } 41 | 42 | try { 43 | $this->runMigrations(); 44 | } catch (Throwable $e) { 45 | if ($this->input->getOption('graceful')) { 46 | $this->output->warning($e->getMessage()); 47 | 48 | return 0; 49 | } 50 | 51 | throw $e; 52 | } 53 | 54 | return 0; 55 | } 56 | 57 | /** 58 | * Run the pending migrations. 59 | */ 60 | protected function runMigrations() 61 | { 62 | $this->prepareDatabase(); 63 | 64 | // Next, we will check to see if a path option has been defined. If it has 65 | // we will use the path relative to the root of this installation folder 66 | // so that migrations may be run for any path within the applications. 67 | $this->migrator->setOutput($this->output) 68 | ->run($this->getMigrationPaths(), [ 69 | 'pretend' => $this->input->getOption('pretend'), 70 | 'step' => $this->input->getOption('step'), 71 | ]); 72 | 73 | // Finally, if the "seed" option has been given, we will re-run the database 74 | // seed task to re-populate the database, which is convenient when adding 75 | // a migration and a seed at the same time, as it is only this command. 76 | if ($this->input->getOption('seed') && ! $this->input->getOption('pretend')) { 77 | $this->call('db:seed', ['--force' => true]); 78 | } 79 | } 80 | 81 | protected function getOptions(): array 82 | { 83 | return [ 84 | ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], 85 | ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], 86 | ['path', null, InputOption::VALUE_OPTIONAL, 'The path to the migrations files to be executed'], 87 | ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], 88 | ['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run'], 89 | ['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run'], 90 | ['step', null, InputOption::VALUE_NONE, 'Force the migrations to be run so they can be rolled back individually'], 91 | ['graceful', null, InputOption::VALUE_NONE, 'Return a successful exit code even if an error occurs'], 92 | ]; 93 | } 94 | 95 | /** 96 | * Prepare the migration database for running. 97 | */ 98 | protected function prepareDatabase() 99 | { 100 | $this->migrator->setConnection($this->input->getOption('database') ?? 'default'); 101 | 102 | if (! $this->migrator->repositoryExists()) { 103 | $this->call('migrate:install', array_filter([ 104 | '--database' => $this->input->getOption('database'), 105 | ])); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Commands/Ast/ModelRewriteTimestampsVisitor.php: -------------------------------------------------------------------------------- 1 | getClass(); 35 | $this->class = new $class(); 36 | } 37 | 38 | public function leaveNode(Node $node) 39 | { 40 | switch ($node) { 41 | case $node instanceof Node\Stmt\Property: 42 | if ($node->props[0]->name->toLowerString() === 'timestamps') { 43 | $this->hasTimestamps = true; 44 | if (! ($node = $this->rewriteTimestamps($node))) { 45 | return NodeTraverser::REMOVE_NODE; 46 | } 47 | } 48 | return $node; 49 | } 50 | 51 | return null; 52 | } 53 | 54 | public function afterTraverse(array $nodes) 55 | { 56 | if ($this->hasTimestamps || $this->shouldRemovedTimestamps()) { 57 | return null; 58 | } 59 | 60 | foreach ($nodes as $namespace) { 61 | if (! $namespace instanceof Node\Stmt\Namespace_) { 62 | continue; 63 | } 64 | 65 | foreach ($namespace->stmts as $class) { 66 | if (! $class instanceof Node\Stmt\Class_) { 67 | continue; 68 | } 69 | 70 | foreach ($class->stmts as $key => $node) { 71 | if (isset($node->props, $node->props[0], $node->props[0]->name) 72 | && $node->props[0]->name->toLowerString() === 'table') { 73 | $newNode = $this->rewriteTimestamps(); 74 | array_splice($class->stmts, $key, 0, [$newNode]); 75 | return null; 76 | } 77 | } 78 | } 79 | } 80 | 81 | return null; 82 | } 83 | 84 | protected function rewriteTimestamps(?Node\Stmt\Property $node = null): ?Node\Stmt\Property 85 | { 86 | if ($this->shouldRemovedTimestamps()) { 87 | return null; 88 | } 89 | 90 | $timestamps = $this->usesTimestamps() ? 'true' : 'false'; 91 | $expr = new Node\Expr\ConstFetch(new Node\Name($timestamps)); 92 | if ($node) { 93 | $node->props[0]->default = $expr; 94 | } else { 95 | $prop = new Node\Stmt\PropertyProperty('timestamps', $expr); 96 | $node = new Node\Stmt\Property(Node\Stmt\Class_::MODIFIER_PUBLIC, [$prop]); 97 | $node->type = new Identifier('bool'); 98 | } 99 | 100 | return $node; 101 | } 102 | 103 | protected function usesTimestamps(): bool 104 | { 105 | $createdAt = $this->class->getCreatedAtColumn(); 106 | $updatedAt = $this->class->getUpdatedAtColumn(); 107 | $columns = Collection::make($this->data->getColumns()); 108 | 109 | return $columns->where('column_name', $createdAt)->count() && $columns->where('column_name', $updatedAt)->count(); 110 | } 111 | 112 | protected function shouldRemovedTimestamps(): bool 113 | { 114 | $useTimestamps = $this->usesTimestamps(); 115 | $ref = new ReflectionClass(get_class($this->class)); 116 | 117 | if (! $ref->getParentClass()) { 118 | return false; 119 | } 120 | 121 | return $useTimestamps == ($ref->getParentClass()->getDefaultProperties()['timestamps'] ?? null); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Commands/Ast/ModelRewriteSoftDeletesVisitor.php: -------------------------------------------------------------------------------- 1 | uses[0]->name->toString() === SoftDeletes::class) { 35 | $this->hasSoftDeletesUse = true; 36 | if (! ($node = $this->rewriteSoftDeletesUse($node))) { 37 | return NodeTraverser::REMOVE_NODE; 38 | } 39 | } 40 | return $node; 41 | case $node instanceof Node\Stmt\TraitUse: 42 | foreach ($node->traits as $trait) { 43 | if ($trait->toString() === 'SoftDeletes' || Str::endsWith($trait->toString(), '\SoftDeletes')) { 44 | $this->hasSoftDeletesTraitUse = true; 45 | if (! ($node = $this->rewriteSoftDeletesTraitUse($node))) { 46 | return NodeTraverser::REMOVE_NODE; 47 | } 48 | } 49 | } 50 | return $node; 51 | } 52 | 53 | return null; 54 | } 55 | 56 | public function afterTraverse(array $nodes) 57 | { 58 | foreach ($nodes as $namespace) { 59 | if (! $namespace instanceof Node\Stmt\Namespace_) { 60 | continue; 61 | } 62 | 63 | if (! $this->hasSoftDeletesUse && ($newUse = $this->rewriteSoftDeletesUse())) { 64 | array_unshift($namespace->stmts, $newUse); 65 | } 66 | 67 | foreach ($namespace->stmts as $class) { 68 | if (! $class instanceof Node\Stmt\Class_) { 69 | continue; 70 | } 71 | 72 | if (! $this->hasSoftDeletesTraitUse && ($newTraitUse = $this->rewriteSoftDeletesTraitUse())) { 73 | array_unshift($class->stmts, $newTraitUse); 74 | } 75 | } 76 | } 77 | 78 | return null; 79 | } 80 | 81 | protected function rewriteSoftDeletesUse(?Node\Stmt\Use_ $node = null): ?Node\Stmt\Use_ 82 | { 83 | if ($this->shouldRemovedSoftDeletes()) { 84 | return null; 85 | } 86 | 87 | if (is_null($node)) { 88 | $use = new Node\Stmt\UseUse(new Node\Name(SoftDeletes::class)); 89 | $node = new Node\Stmt\Use_([$use]); 90 | } 91 | 92 | return $node; 93 | } 94 | 95 | protected function rewriteSoftDeletesTraitUse(?Node\Stmt\TraitUse $node = null): ?Node\Stmt\TraitUse 96 | { 97 | if ($this->shouldRemovedSoftDeletes()) { 98 | return null; 99 | } 100 | 101 | if (is_null($node)) { 102 | $node = new Node\Stmt\TraitUse([new Node\Name('SoftDeletes')]); 103 | } 104 | 105 | return $node; 106 | } 107 | 108 | protected function useSoftDeletes(): bool 109 | { 110 | $model = $this->data->getClass(); 111 | $deletedAt = defined("{$model}::DELETED_AT") ? $model::DELETED_AT : 'deleted_at'; 112 | return Collection::make($this->data->getColumns())->where('column_name', $deletedAt)->count() > 0; 113 | } 114 | 115 | protected function shouldRemovedSoftDeletes(): bool 116 | { 117 | $useSoftDeletes = $this->useSoftDeletes(); 118 | $ref = new ReflectionClass($this->data->getClass()); 119 | 120 | if (! $ref->getParentClass()) { 121 | return false; 122 | } 123 | 124 | return $useSoftDeletes == $ref->getParentClass()->hasMethod('getDeletedAtColumn'); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/DBAL/Connection.php: -------------------------------------------------------------------------------- 1 | connection->exec($sql); 44 | 45 | assert($result !== false); 46 | 47 | return $result; 48 | } catch (PDOException $exception) { 49 | throw Exception::new($exception); 50 | } 51 | } 52 | 53 | /** 54 | * Prepare a new SQL statement. 55 | */ 56 | public function prepare(string $sql): StatementInterface 57 | { 58 | try { 59 | return $this->createStatement( 60 | $this->connection->prepare($sql) 61 | ); 62 | } catch (PDOException $exception) { 63 | throw Exception::new($exception); 64 | } 65 | } 66 | 67 | /** 68 | * Execute a new query against the connection. 69 | */ 70 | public function query(string $sql): ResultInterface 71 | { 72 | try { 73 | $stmt = $this->connection->query($sql); 74 | 75 | assert($stmt instanceof PDOStatement); 76 | 77 | return new Result($stmt); 78 | } catch (PDOException $exception) { 79 | throw Exception::new($exception); 80 | } 81 | } 82 | 83 | /** 84 | * Get the last insert ID. 85 | * 86 | * @param null|string $name 87 | * @return string 88 | */ 89 | public function lastInsertId($name = null) 90 | { 91 | try { 92 | if ($name === null) { 93 | return $this->connection->lastInsertId(); 94 | } 95 | 96 | return $this->connection->lastInsertId($name); 97 | } catch (PDOException $exception) { 98 | throw Exception::new($exception); 99 | } 100 | } 101 | 102 | /** 103 | * Begin a new database transaction. 104 | */ 105 | public function beginTransaction() 106 | { 107 | return $this->connection->beginTransaction(); 108 | } 109 | 110 | /** 111 | * Commit a database transaction. 112 | */ 113 | public function commit() 114 | { 115 | return $this->connection->commit(); 116 | } 117 | 118 | /** 119 | * Roll back a database transaction. 120 | */ 121 | public function rollBack() 122 | { 123 | return $this->connection->rollBack(); 124 | } 125 | 126 | /** 127 | * Wrap quotes around the given input. 128 | * 129 | * @param string $input 130 | * @param string $type 131 | * @return string 132 | */ 133 | public function quote($input, $type = ParameterType::STRING) 134 | { 135 | return $this->connection->quote($input, $type); 136 | } 137 | 138 | /** 139 | * Get the server version for the connection. 140 | * 141 | * @return string 142 | */ 143 | public function getServerVersion() 144 | { 145 | return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION); 146 | } 147 | 148 | /** 149 | * Get the wrapped PDO connection. 150 | */ 151 | public function getWrappedConnection(): PDO 152 | { 153 | return $this->connection; 154 | } 155 | 156 | /** 157 | * Create a new statement instance. 158 | */ 159 | protected function createStatement(PDOStatement $stmt): Statement 160 | { 161 | return new Statement($stmt); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Commands/Ast/ModelRewriteGetterSetterVisitor.php: -------------------------------------------------------------------------------- 1 | getAllMethodsFromStmts($nodes); 44 | 45 | $this->collectMethods($methods); 46 | 47 | return null; 48 | } 49 | 50 | public function afterTraverse(array $nodes) 51 | { 52 | foreach ($nodes as $namespace) { 53 | if (! $namespace instanceof Node\Stmt\Namespace_) { 54 | continue; 55 | } 56 | 57 | foreach ($namespace->stmts as $class) { 58 | if (! $class instanceof Node\Stmt\Class_) { 59 | continue; 60 | } 61 | 62 | array_push($class->stmts, ...$this->buildGetterAndSetter()); 63 | } 64 | } 65 | 66 | return $nodes; 67 | } 68 | 69 | /** 70 | * @return Node\Stmt\ClassMethod[] 71 | */ 72 | protected function buildGetterAndSetter(): array 73 | { 74 | $stmts = []; 75 | foreach ($this->data->getColumns() as $column) { 76 | if ($name = $column['column_name'] ?? null) { 77 | $getter = getter($name); 78 | if (! in_array($getter, $this->getters)) { 79 | $stmts[] = $this->createGetter($getter, $name); 80 | } 81 | $setter = setter($name); 82 | if (! in_array($setter, $this->setters)) { 83 | $stmts[] = $this->createSetter($setter, $name); 84 | } 85 | } 86 | } 87 | 88 | return $stmts; 89 | } 90 | 91 | protected function createGetter(string $method, string $name): Node\Stmt\ClassMethod 92 | { 93 | $node = new Node\Stmt\ClassMethod($method, ['flags' => Node\Stmt\Class_::MODIFIER_PUBLIC]); 94 | $node->stmts[] = new Node\Stmt\Return_( 95 | new Node\Expr\PropertyFetch( 96 | new Node\Expr\Variable('this'), 97 | new Node\Identifier($name) 98 | ) 99 | ); 100 | 101 | return $node; 102 | } 103 | 104 | protected function createSetter(string $method, string $name): Node\Stmt\ClassMethod 105 | { 106 | $node = new Node\Stmt\ClassMethod($method, [ 107 | 'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC, 108 | 'params' => [new Node\Param(new Node\Expr\Variable($name))], 109 | ]); 110 | $node->stmts[] = new Node\Stmt\Expression( 111 | new Node\Expr\Assign( 112 | new Node\Expr\PropertyFetch( 113 | new Node\Expr\Variable('this'), 114 | new Node\Identifier($name) 115 | ), 116 | new Node\Expr\Variable($name) 117 | ) 118 | ); 119 | $node->stmts[] = new Node\Stmt\Return_( 120 | new Node\Expr\Variable('this') 121 | ); 122 | 123 | return $node; 124 | } 125 | 126 | protected function collectMethods(array $methods) 127 | { 128 | /** @var Node\Stmt\ClassMethod $method */ 129 | foreach ($methods as $method) { 130 | $methodName = $method->name->name; 131 | if (Str::startsWith($methodName, 'get')) { 132 | $this->getters[] = $methodName; 133 | } elseif (Str::startsWith($methodName, 'set')) { 134 | $this->setters[] = $methodName; 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Query/JoinClause.php: -------------------------------------------------------------------------------- 1 | type = $type; 74 | $this->table = $table; 75 | $this->parentClass = get_class($parentQuery); 76 | $this->parentGrammar = $parentQuery->getGrammar(); 77 | $this->parentProcessor = $parentQuery->getProcessor(); 78 | $this->parentConnection = $parentQuery->getConnection(); 79 | 80 | parent::__construct( 81 | $this->parentConnection, 82 | $this->parentGrammar, 83 | $this->parentProcessor 84 | ); 85 | } 86 | 87 | /** 88 | * Add an "on" clause to the join. 89 | * 90 | * On clauses can be chained, e.g. 91 | * 92 | * $join->on('contacts.user_id', '=', 'users.id') 93 | * ->on('contacts.info_id', '=', 'info.id') 94 | * 95 | * will produce the following SQL: 96 | * 97 | * on `contacts`.`user_id` = `users`.`id` and `contacts`.`info_id` = `info`.`id` 98 | * 99 | * @param Closure|string $first 100 | * @param null|string $operator 101 | * @param null|Expression|string $second 102 | * @param string $boolean 103 | * @return $this 104 | * @throws InvalidArgumentException 105 | */ 106 | public function on($first, $operator = null, $second = null, $boolean = 'and') 107 | { 108 | if ($first instanceof Closure) { 109 | return $this->whereNested($first, $boolean); 110 | } 111 | 112 | return $this->whereColumn($first, $operator, $second, $boolean); 113 | } 114 | 115 | /** 116 | * Add an "or on" clause to the join. 117 | * 118 | * @param Closure|string $first 119 | * @param null|string $operator 120 | * @param null|string $second 121 | * @return JoinClause 122 | */ 123 | public function orOn($first, $operator = null, $second = null) 124 | { 125 | return $this->on($first, $operator, $second, 'or'); 126 | } 127 | 128 | /** 129 | * Get a new instance of the join clause builder. 130 | * 131 | * @return JoinClause 132 | */ 133 | public function newQuery() 134 | { 135 | return new static($this->newParentQuery(), $this->type, $this->table); 136 | } 137 | 138 | /** 139 | * Create a new query instance for sub-query. 140 | * 141 | * @return Builder 142 | */ 143 | protected function forSubQuery() 144 | { 145 | return $this->newParentQuery()->newQuery(); 146 | } 147 | 148 | /** 149 | * Create a new parent query instance. 150 | * 151 | * @return Builder 152 | */ 153 | protected function newParentQuery() 154 | { 155 | $class = $this->parentClass; 156 | 157 | return new $class($this->parentConnection, $this->parentGrammar, $this->parentProcessor); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Commands/Migrations/FreshCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Drop all tables and re-run all migrations'); 29 | } 30 | 31 | /** 32 | * Execute the console command. 33 | */ 34 | public function handle() 35 | { 36 | if (! $this->confirmToProceed()) { 37 | return; 38 | } 39 | 40 | $connection = $this->input->getOption('database') ?? 'default'; 41 | 42 | if ($this->input->getOption('drop-views')) { 43 | $this->dropAllViews($connection); 44 | 45 | $this->info('Dropped all views successfully.'); 46 | } 47 | 48 | $this->dropAllTables($connection); 49 | 50 | $this->info('Dropped all tables successfully.'); 51 | 52 | $this->call('migrate', array_filter([ 53 | '--database' => $connection, 54 | '--path' => $this->input->getOption('path'), 55 | '--realpath' => $this->input->getOption('realpath'), 56 | '--force' => true, 57 | '--step' => $this->input->getOption('step'), 58 | ])); 59 | 60 | if ($this->needsSeeding()) { 61 | $this->runSeeder($connection); 62 | } 63 | } 64 | 65 | /** 66 | * Drop all the database tables. 67 | */ 68 | protected function dropAllTables(string $connection) 69 | { 70 | $this->container->get(ConnectionResolverInterface::class) 71 | ->connection($connection) 72 | ->getSchemaBuilder() 73 | ->dropAllTables(); 74 | } 75 | 76 | /** 77 | * Drop all the database views. 78 | */ 79 | protected function dropAllViews(string $connection) 80 | { 81 | $this->container->get(ConnectionResolverInterface::class) 82 | ->connection($connection) 83 | ->getSchemaBuilder() 84 | ->dropAllViews(); 85 | } 86 | 87 | /** 88 | * Determine if the developer has requested database seeding. 89 | */ 90 | protected function needsSeeding(): bool 91 | { 92 | return $this->input->getOption('seed') || $this->input->getOption('seeder'); 93 | } 94 | 95 | /** 96 | * Run the database seeder command. 97 | */ 98 | protected function runSeeder(string $database) 99 | { 100 | $this->call('db:seed', array_filter([ 101 | '--database' => $database, 102 | '--force' => true, 103 | ])); 104 | } 105 | 106 | /** 107 | * Get the console command options. 108 | */ 109 | protected function getOptions(): array 110 | { 111 | return [ 112 | ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], 113 | ['drop-views', null, InputOption::VALUE_NONE, 'Drop all tables and views'], 114 | ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], 115 | ['path', null, InputOption::VALUE_OPTIONAL, 'The path to the migrations files to be executed'], 116 | [ 117 | 'realpath', 118 | null, 119 | InputOption::VALUE_NONE, 120 | 'Indicate any provided migration file paths are pre-resolved absolute paths', 121 | ], 122 | ['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run'], 123 | ['seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder'], 124 | [ 125 | 'step', 126 | null, 127 | InputOption::VALUE_NONE, 128 | 'Force the migrations to be run so they can be rolled back individually', 129 | ], 130 | ]; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Commands/Migrations/GenMigrateCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Generate a new migration file'); 30 | } 31 | 32 | /** 33 | * Execute the console command. 34 | */ 35 | public function handle() 36 | { 37 | // It's possible for the developer to specify the tables to modify in this 38 | // schema operation. The developer may also specify if this table needs 39 | // to be freshly created so we can create the appropriate migrations. 40 | $name = Str::snake(trim($this->input->getArgument('name'))); 41 | 42 | $table = $this->input->getOption('table'); 43 | 44 | $create = $this->input->getOption('create') ?: false; 45 | 46 | // If no table was given as an option but a create option is given then we 47 | // will use the "create" option as the table name. This allows the devs 48 | // to pass a table name into this option as a short-cut for creating. 49 | if (! $table && is_string($create)) { 50 | $table = $create; 51 | 52 | $create = true; 53 | } 54 | 55 | // Next, we will attempt to guess the table name if this the migration has 56 | // "create" in the name. This will allow us to provide a convenient way 57 | // of creating migrations that create new tables for the application. 58 | if (! $table) { 59 | [$table, $create] = TableGuesser::guess($name); 60 | } 61 | 62 | // Now we are ready to write the migration out to disk. Once we've written 63 | // the migration out, we will dump-autoload for the entire framework to 64 | // make sure that the migrations are registered by the class loaders. 65 | $this->writeMigration($name, $table, $create); 66 | } 67 | 68 | protected function getArguments(): array 69 | { 70 | return [ 71 | ['name', InputArgument::REQUIRED, 'The name of the migration'], 72 | ]; 73 | } 74 | 75 | protected function getOptions(): array 76 | { 77 | return [ 78 | ['create', null, InputOption::VALUE_OPTIONAL, 'The table to be created'], 79 | ['table', null, InputOption::VALUE_OPTIONAL, 'The table to migrate'], 80 | ['path', null, InputOption::VALUE_OPTIONAL, 'The location where the migration file should be created'], 81 | ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], 82 | ]; 83 | } 84 | 85 | /** 86 | * Write the migration file to disk. 87 | */ 88 | protected function writeMigration(string $name, ?string $table, bool $create): void 89 | { 90 | try { 91 | $file = pathinfo($this->creator->create( 92 | $name, 93 | $this->getMigrationPath(), 94 | $table, 95 | $create 96 | ), PATHINFO_FILENAME); 97 | $this->info("[INFO] Created Migration: {$file}"); 98 | } catch (Throwable $e) { 99 | $this->error("[ERROR] Created Migration: {$e->getMessage()}"); 100 | } 101 | } 102 | 103 | /** 104 | * Get migration path (either specified by '--path' option or default location). 105 | * 106 | * @return string 107 | */ 108 | protected function getMigrationPath() 109 | { 110 | if (! is_null($targetPath = $this->input->getOption('path'))) { 111 | return ! $this->usingRealPath() 112 | ? BASE_PATH . '/' . $targetPath 113 | : $targetPath; 114 | } 115 | 116 | return parent::getMigrationPath(); 117 | } 118 | 119 | /** 120 | * Determine if the given path(s) are pre-resolved "real" paths. 121 | */ 122 | protected function usingRealPath(): bool 123 | { 124 | return $this->input->hasOption('realpath') && $this->input->getOption('realpath'); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Model/SoftDeletingScope.php: -------------------------------------------------------------------------------- 1 | whereNull($model->getQualifiedDeletedAtColumn()); 30 | } 31 | 32 | /** 33 | * Extend the query builder with the needed functions. 34 | */ 35 | public function extend(Builder $builder) 36 | { 37 | foreach ($this->extensions as $extension) { 38 | $this->{"add{$extension}"}($builder); 39 | } 40 | 41 | $builder->onDelete(function (Builder $builder) { 42 | $column = $this->getDeletedAtColumn($builder); 43 | 44 | return $builder->update([ 45 | $column => $builder->getModel()->freshTimestampString(), 46 | ]); 47 | }); 48 | } 49 | 50 | /** 51 | * Get the "deleted at" column for the builder. 52 | * 53 | * @return string 54 | */ 55 | protected function getDeletedAtColumn(Builder $builder) 56 | { 57 | if (count((array) $builder->getQuery()->joins) > 0) { 58 | return $builder->getModel()->getQualifiedDeletedAtColumn(); 59 | } 60 | 61 | return $builder->getModel()->getDeletedAtColumn(); 62 | } 63 | 64 | /** 65 | * Add the restore extension to the builder. 66 | */ 67 | protected function addRestore(Builder $builder) 68 | { 69 | $builder->macro('restore', function (Builder $builder) { 70 | $builder->withTrashed(); 71 | 72 | return $builder->update([$builder->getModel()->getDeletedAtColumn() => null]); 73 | }); 74 | } 75 | 76 | /** 77 | * Add the restore-or-create extension to the builder. 78 | */ 79 | protected function addRestoreOrCreate(Builder $builder) 80 | { 81 | $builder->macro('restoreOrCreate', function (Builder $builder, array $attributes = [], array $values = []) { 82 | $builder->withTrashed(); 83 | 84 | return tap($builder->firstOrCreate($attributes, $values), function ($instance) { 85 | $instance->restore(); 86 | }); 87 | }); 88 | } 89 | 90 | /** 91 | * Add the create-or-restore extension to the builder. 92 | */ 93 | protected function addCreateOrRestore(Builder $builder) 94 | { 95 | $builder->macro('createOrRestore', function (Builder $builder, array $attributes = [], array $values = []) { 96 | $builder->withTrashed(); 97 | 98 | return tap($builder->createOrFirst($attributes, $values), function ($instance) { 99 | $instance->restore(); 100 | }); 101 | }); 102 | } 103 | 104 | /** 105 | * Add the with-trashed extension to the builder. 106 | */ 107 | protected function addWithTrashed(Builder $builder) 108 | { 109 | $builder->macro('withTrashed', function (Builder $builder, $withTrashed = true) { 110 | if (! $withTrashed) { 111 | return $builder->withoutTrashed(); 112 | } 113 | 114 | return $builder->withoutGlobalScope($this); 115 | }); 116 | } 117 | 118 | /** 119 | * Add the without-trashed extension to the builder. 120 | */ 121 | protected function addWithoutTrashed(Builder $builder) 122 | { 123 | $builder->macro('withoutTrashed', function (Builder $builder) { 124 | $model = $builder->getModel(); 125 | 126 | $builder->withoutGlobalScope($this)->whereNull( 127 | $model->getQualifiedDeletedAtColumn() 128 | ); 129 | 130 | return $builder; 131 | }); 132 | } 133 | 134 | /** 135 | * Add the only-trashed extension to the builder. 136 | */ 137 | protected function addOnlyTrashed(Builder $builder) 138 | { 139 | $builder->macro('onlyTrashed', function (Builder $builder) { 140 | $model = $builder->getModel(); 141 | 142 | $builder->withoutGlobalScope($this)->whereNotNull( 143 | $model->getQualifiedDeletedAtColumn() 144 | ); 145 | 146 | return $builder; 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Commands/ModelOption.php: -------------------------------------------------------------------------------- 1 | pool; 50 | } 51 | 52 | public function setPool(string $pool): static 53 | { 54 | $this->pool = $pool; 55 | return $this; 56 | } 57 | 58 | public function getPath(): string 59 | { 60 | return $this->path; 61 | } 62 | 63 | public function setPath(string $path): static 64 | { 65 | $this->path = $path; 66 | return $this; 67 | } 68 | 69 | public function isForceCasts(): bool 70 | { 71 | return $this->forceCasts; 72 | } 73 | 74 | public function setForceCasts(bool $forceCasts): static 75 | { 76 | $this->forceCasts = $forceCasts; 77 | return $this; 78 | } 79 | 80 | public function getPrefix(): string 81 | { 82 | return $this->prefix; 83 | } 84 | 85 | public function setPrefix(string $prefix): static 86 | { 87 | $this->prefix = $prefix; 88 | return $this; 89 | } 90 | 91 | public function getInheritance(): string 92 | { 93 | return $this->inheritance; 94 | } 95 | 96 | public function setInheritance(string $inheritance): static 97 | { 98 | $this->inheritance = $inheritance; 99 | return $this; 100 | } 101 | 102 | public function getUses(): string 103 | { 104 | return $this->uses; 105 | } 106 | 107 | public function setUses(string $uses): static 108 | { 109 | $this->uses = $uses; 110 | return $this; 111 | } 112 | 113 | public function isRefreshFillable(): bool 114 | { 115 | return $this->refreshFillable; 116 | } 117 | 118 | public function setRefreshFillable(bool $refreshFillable): static 119 | { 120 | $this->refreshFillable = $refreshFillable; 121 | return $this; 122 | } 123 | 124 | public function getTableMapping(): array 125 | { 126 | return $this->tableMapping; 127 | } 128 | 129 | public function setTableMapping(array $tableMapping): static 130 | { 131 | foreach ($tableMapping as $item) { 132 | [$key, $name] = explode(':', $item); 133 | $this->tableMapping[$key] = $name; 134 | } 135 | 136 | return $this; 137 | } 138 | 139 | public function getIgnoreTables(): array 140 | { 141 | return $this->ignoreTables; 142 | } 143 | 144 | public function setIgnoreTables(array $ignoreTables): static 145 | { 146 | $this->ignoreTables = $ignoreTables; 147 | return $this; 148 | } 149 | 150 | public function isWithComments(): bool 151 | { 152 | return $this->withComments; 153 | } 154 | 155 | public function setWithComments(bool $withComments): static 156 | { 157 | $this->withComments = $withComments; 158 | return $this; 159 | } 160 | 161 | public function isWithIde(): bool 162 | { 163 | return $this->withIde; 164 | } 165 | 166 | public function setWithIde(bool $withIde): ModelOption 167 | { 168 | $this->withIde = $withIde; 169 | return $this; 170 | } 171 | 172 | public function getVisitors(): array 173 | { 174 | return $this->visitors; 175 | } 176 | 177 | public function setVisitors(array $visitors): static 178 | { 179 | $this->visitors = $visitors; 180 | return $this; 181 | } 182 | 183 | public function isCamelCase(): bool 184 | { 185 | return $this->propertyCase === self::PROPERTY_CAMEL_CASE; 186 | } 187 | 188 | public function setPropertyCase($propertyCase): static 189 | { 190 | $this->propertyCase = (int) $propertyCase; 191 | return $this; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Model/Concerns/GuardsAttributes.php: -------------------------------------------------------------------------------- 1 | fillable; 41 | } 42 | 43 | /** 44 | * Set the fillable attributes for the model. 45 | */ 46 | public function fillable(array $fillable): static 47 | { 48 | $this->fillable = $fillable; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Get the guarded attributes for the model. 55 | */ 56 | public function getGuarded(): array 57 | { 58 | return $this->guarded; 59 | } 60 | 61 | /** 62 | * Set the guarded attributes for the model. 63 | */ 64 | public function guard(array $guarded): static 65 | { 66 | $this->guarded = $guarded; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Disable all mass assignable restrictions. 73 | * @param mixed $state 74 | */ 75 | public static function unguard($state = true): void 76 | { 77 | static::$unguarded = $state; 78 | } 79 | 80 | /** 81 | * Enable the mass assignment restrictions. 82 | */ 83 | public static function reguard(): void 84 | { 85 | static::$unguarded = false; 86 | } 87 | 88 | /** 89 | * Determine if current state is "unguarded". 90 | */ 91 | public static function isUnguarded(): bool 92 | { 93 | return static::$unguarded; 94 | } 95 | 96 | /** 97 | * Run the given callable while being unguarded. 98 | */ 99 | public static function unguarded(callable $callback) 100 | { 101 | if (static::$unguarded) { 102 | return $callback(); 103 | } 104 | 105 | static::unguard(); 106 | 107 | try { 108 | return $callback(); 109 | } finally { 110 | static::reguard(); 111 | } 112 | } 113 | 114 | /** 115 | * Determine if the given attribute may be mass assigned. 116 | */ 117 | public function isFillable(string $key): bool 118 | { 119 | if (static::$unguarded) { 120 | return true; 121 | } 122 | 123 | // If the key is in the "fillable" array, we can of course assume that it's 124 | // a fillable attribute. Otherwise, we will check the guarded array when 125 | // we need to determine if the attribute is black-listed on the model. 126 | if (in_array($key, $this->getFillable())) { 127 | return true; 128 | } 129 | 130 | // If the attribute is explicitly listed in the "guarded" array then we can 131 | // return false immediately. This means this attribute is definitely not 132 | // fillable and there is no point in going any further in this method. 133 | if ($this->isGuarded($key)) { 134 | return false; 135 | } 136 | 137 | return empty($this->getFillable()) 138 | && ! Str::startsWith($key, '_'); 139 | } 140 | 141 | /** 142 | * Determine if the given key is guarded. 143 | * 144 | * @return bool 145 | */ 146 | public function isGuarded(string $key) 147 | { 148 | return in_array($key, $this->getGuarded()) || $this->getGuarded() == ['*']; 149 | } 150 | 151 | /** 152 | * Determine if the model is totally guarded. 153 | * 154 | * @return bool 155 | */ 156 | public function totallyGuarded() 157 | { 158 | return count($this->getFillable()) === 0 && $this->getGuarded() == ['*']; 159 | } 160 | 161 | /** 162 | * Get the fillable attributes of a given array. 163 | * 164 | * @return array 165 | */ 166 | protected function fillableFromArray(array $attributes) 167 | { 168 | if (count($this->getFillable()) > 0 && ! static::$unguarded) { 169 | return array_intersect_key($attributes, array_flip($this->getFillable())); 170 | } 171 | 172 | return $attributes; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Commands/Migrations/RefreshCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Reset and re-run all migrations'); 27 | } 28 | 29 | /** 30 | * Execute the console command. 31 | */ 32 | public function handle() 33 | { 34 | if (! $this->confirmToProceed()) { 35 | return; 36 | } 37 | 38 | // Next we'll gather some of the options so that we can have the right options 39 | // to pass to the commands. This includes options such as which database to 40 | // use and the path to use for the migration. Then we'll run the command. 41 | $connection = $this->input->getOption('database') ?? 'default'; 42 | 43 | $path = $this->input->getOption('path') ?? ''; 44 | 45 | // If the "step" option is specified it means we only want to rollback a small 46 | // number of migrations before migrating again. For example, the user might 47 | // only rollback and remigrate the latest four migrations instead of all. 48 | $step = (int) $this->input->getOption('step') ?: 0; 49 | 50 | if ($step > 0) { 51 | $this->runRollback($connection, $path, $step); 52 | } else { 53 | $this->runReset($connection, $path); 54 | } 55 | 56 | // The refresh command is essentially just a brief aggregate of a few other of 57 | // the migration commands and just provides a convenient wrapper to execute 58 | // them in succession. We'll also see if we need to re-seed the database. 59 | $this->call('migrate', array_filter([ 60 | '--database' => $connection, 61 | '--path' => $path, 62 | '--realpath' => $this->input->getOption('realpath'), 63 | '--force' => true, 64 | ])); 65 | 66 | if ($this->needsSeeding()) { 67 | $this->runSeeder($connection); 68 | } 69 | } 70 | 71 | /** 72 | * Run the rollback command. 73 | */ 74 | protected function runRollback(string $database, string $path, int $step): void 75 | { 76 | $this->call('migrate:rollback', array_filter([ 77 | '--database' => $database, 78 | '--path' => $path, 79 | '--realpath' => $this->input->getOption('realpath'), 80 | '--step' => $step, 81 | '--force' => true, 82 | ])); 83 | } 84 | 85 | /** 86 | * Run the reset command. 87 | */ 88 | protected function runReset(string $database, string $path): void 89 | { 90 | $this->call('migrate:reset', array_filter([ 91 | '--database' => $database, 92 | '--path' => $path, 93 | '--realpath' => $this->input->getOption('realpath'), 94 | '--force' => true, 95 | ])); 96 | } 97 | 98 | /** 99 | * Determine if the developer has requested database seeding. 100 | */ 101 | protected function needsSeeding(): bool 102 | { 103 | return $this->input->getOption('seed') || $this->input->getOption('seeder'); 104 | } 105 | 106 | /** 107 | * Run the database seeder command. 108 | */ 109 | protected function runSeeder(string $database): void 110 | { 111 | $this->call('db:seed', array_filter([ 112 | '--database' => $database, 113 | '--force' => true, 114 | ])); 115 | } 116 | 117 | /** 118 | * Get the console command options. 119 | * 120 | * @return array 121 | */ 122 | protected function getOptions() 123 | { 124 | return [ 125 | ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], 126 | ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], 127 | ['path', null, InputOption::VALUE_OPTIONAL, 'The path to the migrations files to be executed'], 128 | ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], 129 | ['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run'], 130 | ['seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder'], 131 | ['step', null, InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted & re-run'], 132 | ]; 133 | } 134 | } 135 | --------------------------------------------------------------------------------