Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/AbstractActiveRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
*
* See {@see ActiveRecord} for a concrete implementation.
*
* @psalm-import-type ModelClass from ActiveQuery
* @psalm-import-type RawFrom from QueryInterface
*/
abstract class AbstractActiveRecord implements ActiveRecordInterface
Expand All @@ -61,6 +60,12 @@
/** @var string[][] */
private array $relationsDependencies = [];

/**
* @template T as ActiveRecordInterface
* @template TModelClass as T|class-string<T>|null
* @psalm-param TModelClass $modelClass
* @psalm-return ActiveQueryInterface<(TModelClass is null ? static : T), null>
*/
Comment thread
Tigrov marked this conversation as resolved.
public function createQuery(ActiveRecordInterface|string|null $modelClass = null): ActiveQueryInterface
{
$modelClass ??= $this;
Expand Down Expand Up @@ -340,7 +345,7 @@
} else {
$link = $relation->getLink();
$p1 = $linkModel->isPrimaryKey(array_keys($link));
$p2 = $this->isPrimaryKey(array_values($link));

Check warning on line 348 in src/AbstractActiveRecord.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "UnwrapArrayValues": @@ @@ } else { $link = $relation->getLink(); $p1 = $linkModel->isPrimaryKey(array_keys($link)); - $p2 = $this->isPrimaryKey(array_values($link)); + $p2 = $this->isPrimaryKey($link); if ($p1 && $p2) { if ($this->isNew() && $linkModel->isNew()) {

if ($p1 && $p2) {
if ($this->isNew() && $linkModel->isNew()) {
Expand All @@ -348,7 +353,7 @@
}

if ($this->isNew()) {
$this->bindModels(array_flip($link), $this, $linkModel);

Check warning on line 356 in src/AbstractActiveRecord.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "UnwrapArrayFlip": @@ @@ } if ($this->isNew()) { - $this->bindModels(array_flip($link), $this, $linkModel); + $this->bindModels($link, $this, $linkModel); } else { $this->bindModels($link, $linkModel, $this); }
} else {
$this->bindModels($link, $linkModel, $this);
}
Expand Down Expand Up @@ -419,8 +424,15 @@
$this->related[$name] = $records;
}

/**
* @template T as ActiveRecordInterface
* @template TModelClass as T|class-string<T>|null
* @psalm-param TModelClass $modelClass
* @psalm-return ActiveQuery<(TModelClass is null ? static : T), null>
*/
Comment thread
Tigrov marked this conversation as resolved.
public static function query(ActiveRecordInterface|string|null $modelClass = null): ActiveQueryInterface
{
/** @psalm-var ActiveQuery<(TModelClass is null ? static : T), null> */
return new ActiveQuery($modelClass ?? static::class);
}

Expand Down Expand Up @@ -453,7 +465,7 @@

public function resetRelation(string $name): void
{
foreach ($this->relationsDependencies as &$relationNames) {

Check warning on line 468 in src/AbstractActiveRecord.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "Foreach_": @@ @@ public function resetRelation(string $name): void { - foreach ($this->relationsDependencies as &$relationNames) { + foreach ([] as &$relationNames) { unset($relationNames[$name]); }
unset($relationNames[$name]);
}

Expand All @@ -474,7 +486,7 @@
{
if (
isset($this->relationsDependencies[$propertyName])
&& ($value === null || $this->get($propertyName) !== $value)

Check warning on line 489 in src/AbstractActiveRecord.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "Identical": @@ @@ { if ( isset($this->relationsDependencies[$propertyName]) - && ($value === null || $this->get($propertyName) !== $value) + && ($value !== null || $this->get($propertyName) !== $value) ) { $this->resetDependentRelations($propertyName); }
) {
$this->resetDependentRelations($propertyName);
}
Expand All @@ -484,7 +496,7 @@

public function populateProperties(array $values): void
{
$values = array_intersect_key($values, array_flip($this->propertyNames()));

Check warning on line 499 in src/AbstractActiveRecord.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "UnwrapArrayIntersectKey": @@ @@ public function populateProperties(array $values): void { - $values = array_intersect_key($values, array_flip($this->propertyNames())); + $values = $values; foreach ($values as $name => $value) { $this->populateProperty($name, $value); }
foreach ($values as $name => $value) {
$this->populateProperty($name, $value);
}
Expand Down Expand Up @@ -650,7 +662,7 @@
}
}

if (!$relation->isMultiple()) {

Check warning on line 665 in src/AbstractActiveRecord.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "LogicalNot": @@ @@ } } - if (!$relation->isMultiple()) { + if ($relation->isMultiple()) { unset($this->related[$relationName]); } elseif (isset($this->related[$relationName]) && is_array($this->related[$relationName])) { /** @psalm-var array<array-key, ActiveRecordInterface> $related */
unset($this->related[$relationName]);
} elseif (isset($this->related[$relationName]) && is_array($this->related[$relationName])) {
/** @psalm-var array<array-key, ActiveRecordInterface> $related */
Expand Down Expand Up @@ -822,17 +834,20 @@
*
* @return ActiveQueryInterface The relational query object.
*
* @psalm-param ModelClass $modelClass
* @template T as ActiveRecordInterface
* @psalm-param T|class-string<T> $modelClass
* @psalm-param array<string, string> $link
* @psalm-return ActiveQueryInterface<T, null>
*
Comment thread
Tigrov marked this conversation as resolved.
* @see AbstractActiveRecord::hasOne()
* @see AbstractActiveRecord::hasMany()
*/
protected function createRelationQuery(

Check warning on line 845 in src/AbstractActiveRecord.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "ProtectedVisibility": @@ @@ * @see AbstractActiveRecord::hasOne() * @see AbstractActiveRecord::hasMany() */ - protected function createRelationQuery( + private function createRelationQuery( ActiveRecordInterface|string $modelClass, array $link, bool $multiple,
ActiveRecordInterface|string $modelClass,
array $link,
bool $multiple,
): ActiveQueryInterface {
/** @psalm-var ActiveQueryInterface<T, null> */
return $this->createQuery($modelClass)->primaryModel($this)->link($link)->multiple($multiple);
}

Expand All @@ -844,7 +859,7 @@
*
* @see AbstractActiveRecord::delete()
*/
protected function deleteInternal(): int

Check warning on line 862 in src/AbstractActiveRecord.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "ProtectedVisibility": @@ @@ * * @see AbstractActiveRecord::delete() */ - protected function deleteInternal(): int + private function deleteInternal(): int { /** * We don't check the return value of deleteAll() because it is possible the record is already deleted in
{
/**
* We don't check the return value of deleteAll() because it is possible the record is already deleted in
Expand Down Expand Up @@ -915,7 +930,7 @@
*
* @see AbstractActiveRecord::refresh()
*/
protected function refreshInternal(array|ActiveRecordInterface|null $record = null): bool

Check warning on line 933 in src/AbstractActiveRecord.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "ProtectedVisibility": @@ @@ * * @see AbstractActiveRecord::refresh() */ - protected function refreshInternal(array|ActiveRecordInterface|null $record = null): bool + private function refreshInternal(array|ActiveRecordInterface|null $record = null): bool { if ($record === null || is_array($record)) { return false;
{
if ($record === null || is_array($record)) {
return false;
Expand All @@ -942,7 +957,7 @@
*
* @see AbstractActiveRecord::update()
*/
protected function updateInternal(?array $properties = null): int

Check warning on line 960 in src/AbstractActiveRecord.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "ProtectedVisibility": @@ @@ * * @see AbstractActiveRecord::update() */ - protected function updateInternal(?array $properties = null): int + private function updateInternal(?array $properties = null): int { if ($this->isNew()) { throw new InvalidCallException('The record is new and cannot be updated.');
{
if ($this->isNew()) {
throw new InvalidCallException('The record is new and cannot be updated.');
Expand Down
46 changes: 32 additions & 14 deletions src/ActiveQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,23 @@
* and {@see ActiveQuery::on()} which adds a condition that is to be added to relational
* query join condition.
*
* @psalm-type ModelClass = ActiveRecordInterface|class-string<ActiveRecordInterface>
* @template TModel as ActiveRecordInterface
* @template TAsArray as ?bool
* @implements ActiveQueryInterface<TModel, TAsArray>
* @psalm-import-type IndexBy from QueryInterface
* @psalm-import-type Join from QueryInterface
* @psalm-import-type ActiveQueryResult from ActiveQueryInterface
* @psalm-import-type Via from ActiveQueryInterface
*
* @psalm-property IndexBy|null $indexBy
* @psalm-suppress ClassMustBeFinal
*/
class ActiveQuery extends Query implements ActiveQueryInterface
{
/** @psalm-var TModel $model */
private ActiveRecordInterface $model;
private ?string $sql = null;
private array|ExpressionInterface|string|null $on = null;
/** @psalm-var TAsArray $asArray */
private ?bool $asArray = null;
private array $with = [];
private bool $multiple = false;
Expand Down Expand Up @@ -155,7 +158,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
private array|ActiveQueryInterface|null $via = null;

/**
* @psalm-param ModelClass $modelClass
* @psalm-param TModel|class-string<TModel> $modelClass
*/
final public function __construct(
ActiveRecordInterface|string $modelClass,
Expand All @@ -180,9 +183,17 @@ public function __clone()
}
}

/**
* @template T as ?bool
* @psalm-param T $value
* @psalm-return static<TModel, T>
*/
public function asArray(?bool $value = true): static
{
/** @psalm-suppress InvalidPropertyAssignmentValue */
$this->asArray = $value;

/** @psalm-var static<TModel, T> */
return $this;
}

Expand Down Expand Up @@ -325,11 +336,13 @@ public function populate(array $rows): array
$this->findWith($this->with, $models);
}

/** @psalm-var non-empty-list<(TAsArray is true ? array<string, mixed> : TModel)> $models */
$this->addInverseRelations($models);

return $models;
}

/** @psalm-return (TAsArray is true ? array<string, mixed> : TModel)|null */
public function one(): array|ActiveRecordInterface|null
{
if ($this->shouldEmulateExecution()) {
Expand Down Expand Up @@ -451,6 +464,7 @@ public function viaTable(string $tableName, array $link, ?callable $callable = n
{
$model = $this->primaryModel ?? $this->model;

/** @psalm-suppress UnsafeGenericInstantiation, InvalidTemplateParam */
$relation = (new static($model))
->from([$tableName])
->link($link)
Expand Down Expand Up @@ -509,6 +523,7 @@ public function getSql(): ?string
return $this->sql;
}

/** @psalm-return (TAsArray is true ? array<string, mixed> : TModel)|null */
public function findByPk(array|float|int|string $values): array|ActiveRecordInterface|null
{
$values = (array) $values;
Expand Down Expand Up @@ -543,6 +558,7 @@ public function sql(?string $value): static
return $this;
}

/** @psalm-return TModel */
public function getModel(): ActiveRecordInterface
{
return clone $this->model;
Expand Down Expand Up @@ -686,7 +702,7 @@ protected function index(array $rows): array
* @return ActiveRecordInterface[]|array[] The model instances.
*
* @psalm-param non-empty-list<array<string, mixed>> $rows
* @psalm-return non-empty-list<ActiveQueryResult>
* @psalm-return non-empty-list<(TAsArray is true ? array<string, mixed> : TModel)>
*/
protected function createModels(array $rows): array
{
Expand All @@ -702,12 +718,12 @@ protected function createModels(array $rows): array
$rows = ($this->resultCallback)($rows);

if ($rows[0] instanceof ActiveRecordInterface) {
/** @psalm-var non-empty-list<ActiveRecordInterface> */
/** @psalm-var non-empty-list<TModel> */
return $rows;
}
}
/** @var non-empty-list<array<string, mixed>> $rows */

/** @psalm-var non-empty-list<array<string, mixed>> $rows */
return array_map(
fn(array $row) => $this->getModel()->populateRecord($row),
$rows,
Expand Down Expand Up @@ -760,6 +776,7 @@ private function removeDuplicatedRows(array $rows): array

private function createInstance(): static
{
/** @psalm-suppress UnsafeGenericInstantiation, InvalidTemplateParam */
return (new static($this->model))
->where($this->getWhere())
->limit($this->getLimit())
Expand All @@ -780,7 +797,7 @@ private function createInstance(): static

/**
* @psalm-param array<string, mixed> $row
* @psalm-return ActiveQueryResult
* @psalm-return (TAsArray is true ? array<string, mixed> : TModel)
*/
private function populateOne(array $row): ActiveRecordInterface|array
{
Expand All @@ -800,8 +817,8 @@ private function populateOne(array $row): ActiveRecordInterface|array
* @throws ReflectionException
* @throws Throwable
*
* @psalm-param non-empty-list<ActiveQueryResult> $models
* @psalm-param-out non-empty-list<ActiveQueryResult> $models
* @psalm-param non-empty-list<ActiveRecordInterface|array<string, mixed>> $models
* @psalm-param-out non-empty-list<ActiveRecordInterface|array<string, mixed>> $models
*/
private function findWith(array $with, array &$models): void
{
Expand Down Expand Up @@ -872,14 +889,13 @@ private function normalizeRelations(ActiveRecordInterface $model, array $with):
* @param ActiveRecordInterface[]|array[] $result the array of related records as generated
* by {@see ActiveQuery::populate()}
*
* @throws InvalidConfigException
*
* @psalm-param non-empty-list<ActiveQueryResult> $result
* @psalm-param-out non-empty-list<ActiveQueryResult> $result
* @psalm-param non-empty-list<(TAsArray is true ? array<string, mixed> : TModel)> $result
* @psalm-param-out non-empty-list<(TAsArray is true ? array<string, mixed> : TModel)> $result
*/
private function addInverseRelations(array &$result): void
{
if ($this->inverseOf === null) {
/** @psalm-var non-empty-list<(TAsArray is true ? array<string, mixed> : TModel)> $result */
return;
}

Expand All @@ -889,7 +905,7 @@ private function addInverseRelations(array &$result): void
$inverseRelation = $relatedModel->relationQuery($this->inverseOf);
$primaryModel = $inverseRelation->isMultiple() ? [$this->primaryModel] : $this->primaryModel;

/** @var ActiveRecordInterface $relatedModel */
/** @psalm-var TModel $relatedModel */
foreach ($result as $relatedModel) {
$relatedModel->populateRelation($this->inverseOf, $primaryModel);
}
Expand All @@ -902,5 +918,7 @@ private function addInverseRelations(array &$result): void
$relatedModel[$this->inverseOf] = $primaryModel;
}
}

/** @psalm-var non-empty-list<(TAsArray is true ? array<string, mixed> : TModel)> $result */
}
}
36 changes: 28 additions & 8 deletions src/ActiveQueryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@
/**
* A common interface to be implemented by active record query classes.
*
* That are methods for all normal queries that return active records but also relational queries in which the query
* These are methods for all normal queries that return active records but also relational queries in which the query
* represents a relation between two active record classes and will return related records only.
*
* @template TModel as ActiveRecordInterface
* @template TAsArray as ?bool
* @psalm-type Via = array{string, ActiveQueryInterface, bool}|ActiveQueryInterface
Comment thread
Tigrov marked this conversation as resolved.
* @psalm-type ActiveQueryResult = ActiveRecordInterface|array<string, mixed>
*/
interface ActiveQueryInterface extends QueryInterface
{
Expand All @@ -39,16 +40,20 @@ interface ActiveQueryInterface extends QueryInterface
* on {@see ActiveQueryInterface::isAsArray()} result.
* Empty array if the query results in nothing.
*
* @psalm-return array<ActiveRecordInterface|array>
* @psalm-return (TAsArray is true ? array<array<string, mixed>> : TModel[])
*/
public function all(): array;

/**
* Sets the {@see ActiveQuery::$asArray} property.
*
* @param bool|null $value Whether to return the query results in terms of arrays instead of Active Records.
* @param ?bool $value Whether to return the query results in terms of arrays instead of Active Records.
*
* @return static The query object itself.
*
* @template T as ?bool
* @psalm-param T $value
* @psalm-return static<TModel, T>
*/
public function asArray(?bool $value = true): static;

Expand Down Expand Up @@ -217,6 +222,7 @@ public function getJoinsWith(): array;
*
* @psalm-param array<string|callable(ActiveQueryInterface):void>|string $with
Comment thread
Tigrov marked this conversation as resolved.
* @psalm-param array<string,string>|string $joinType
* @psalm-return static<TModel, TAsArray>
*/
public function joinWith(
array|string $with,
Expand Down Expand Up @@ -372,8 +378,8 @@ public function sql(?string $value): static;
* @psalm-param list<array<string, mixed>> $rows
* @psalm-return (
* $rows is non-empty-list<array<string, mixed>>
* ? non-empty-list<ActiveQueryResult>
* : list<ActiveQueryResult>
* ? non-empty-list<(TAsArray is true ? array<string, mixed> : TModel)>
* : list<(TAsArray is true ? array<string, mixed> : TModel)>
* )
*/
public function populate(array $rows): array;
Expand Down Expand Up @@ -446,6 +452,12 @@ public function getInverseOf(): ?string;
* @throws Throwable if the relation is invalid.
*
* @return ActiveRecordInterface|ActiveRecordInterface[]|array|array[]|null the related record(s).
*
* @psalm-return (
* TAsArray is true
* ? array<string, mixed>|array<array<string, mixed>>
* : TModel|TModel[]
* )|null
*/
public function relatedRecords(): ActiveRecordInterface|array|null;

Expand Down Expand Up @@ -483,12 +495,16 @@ public function relatedRecords(): ActiveRecordInterface|array|null;
* $customer = $customerQuery->findByPk($id);
* }
* ```
*
* @psalm-return (TAsArray is true ? array<string, mixed> : TModel)|null
*/
public function findByPk(array|float|int|string $values): array|ActiveRecordInterface|null;

/**
* Returns a value indicating whether the query result rows should be returned as arrays instead of Active Record
* models.
*
* @psalm-return TAsArray
*/
public function isAsArray(): ?bool;

Expand Down Expand Up @@ -547,6 +563,8 @@ public function getLink(): array;

/**
* @return ActiveRecordInterface The model instance associated with this query.
*
* @psalm-return TModel
*/
public function getModel(): ActiveRecordInterface;

Expand Down Expand Up @@ -581,6 +599,8 @@ public function isMultiple(): bool;
*
* @return ActiveRecordInterface|array|null The first row as an `array` or instance of {@see ActiveRecordInterface}
* of the query result, depends on {@see ActiveQueryInterface::isAsArray()} result. `null` if the query results in nothing.
*
* @psalm-return (TAsArray is true ? array<string, mixed> : TModel)|null
*/
public function one(): array|ActiveRecordInterface|null;

Expand All @@ -595,8 +615,8 @@ public function one(): array|ActiveRecordInterface|null;
* {@see ActiveQueryInterface::link()} is invalid.
* @return ActiveRecordInterface[]|array[] The related models.
*
* @psalm-param non-empty-list<ActiveQueryResult> $primaryModels
* @psalm-param-out non-empty-list<ActiveQueryResult> $primaryModels
* @psalm-param non-empty-list<ActiveRecordInterface|array<string, mixed>> $primaryModels
* @psalm-param-out non-empty-list<ActiveRecordInterface|array<string, mixed>> $primaryModels
*/
public function populateRelation(string $name, array &$primaryModels): array;
}
Loading
Loading