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