Skip to content

Commit

Permalink
Rework query interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
olvlvl committed Nov 2, 2024
1 parent e62504d commit 2a4d60d
Show file tree
Hide file tree
Showing 10 changed files with 1,336 additions and 1,719 deletions.
1,029 changes: 7 additions & 1,022 deletions README.md

Large diffs are not rendered by default.

884 changes: 884 additions & 0 deletions docs/ActiveRecord/Query.md

Large diffs are not rendered by default.

146 changes: 22 additions & 124 deletions lib/ActiveRecord/Model.php
Original file line number Diff line number Diff line change
@@ -1,33 +1,16 @@
<?php

/*
* This file is part of the ICanBoogie package.
*
* (c) Olivier Laviale <olivier.laviale@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace ICanBoogie\ActiveRecord;

use AllowDynamicProperties;
use ArrayAccess;
use ICanBoogie\ActiveRecord;
use ICanBoogie\ActiveRecord\Config\ModelDefinition;
use ICanBoogie\OffsetNotWritable;

use function array_fill_keys;
use function array_keys;
use function array_shift;
use function count;
use function func_get_args;
use function get_parent_class;
use function implode;
use function is_array;
use function is_callable;
use function method_exists;
use function sprintf;

/**
* Base class for activerecord models.
Expand All @@ -36,9 +19,7 @@
* @template TValue of ActiveRecord
*
* @property-read Model|null $parent Parent model.
* @property ActiveRecordCache $activerecord_cache The cache use to store activerecords.
*/
#[AllowDynamicProperties]
class Model extends Table
{
/**
Expand All @@ -56,30 +37,19 @@ class Model extends Table
*/
public readonly RelationCollection $relations;

/**
* Returns the records cache.
*
* **Note:** The method needs to be implemented through prototype bindings.
*/
protected function lazy_get_activerecord_cache(): ActiveRecordCache
{
/** @phpstan-ignore-next-line */
return parent::lazy_get_activerecord_cache();
}

public function __construct(
Connection $connection,
public readonly ModelProvider $models,
private readonly ModelDefinition $definition
ModelDefinition $definition
) {
$this->activerecord_class = $this->definition->activerecord_class; // @phpstan-ignore-line
$this->query_class = $this->definition->query_class;
$this->activerecord_class = $definition->activerecord_class; // @phpstan-ignore-line
$this->query_class = $definition->query_class;

$parent = $this->resolve_parent($models);

parent::__construct($connection, $definition->table, $parent);

$this->relations = new RelationCollection($this, $this->definition->association);
$this->relations = new RelationCollection($this, $definition->association);
}

private function resolve_parent(ModelProvider $models): ?Model
Expand All @@ -93,77 +63,47 @@ private function resolve_parent(ModelProvider $models): ?Model
return $models->model_for_record($parent_class); // @phpstan-ignore-line
}

/**
* Handles query methods, dynamic filters, and relations.
*
* @inheritdoc
*/
public function __call($method, $arguments)
{
if (is_callable([ $this->relations, $method ])) {
return $this->relations->$method(...$arguments);
}

return parent::__call($method, $arguments);
}

/**
* Finds a record or a collection of records.
*
* @param mixed $key A key, multiple keys, or an array of keys.
* @param int|non-empty-string ...$keys
*
* @return TValue|TValue[] A record or a set of records.
* @throws RecordNotFound when the record, or one or more records of the records
* set couldn't be found.
*/
public function find(mixed $key)
public function find(int|string ...$keys)
{
$args = func_get_args();
$n = count($args);
if (count($keys) == 1) {
$key = current($keys);

if (!$n) {
throw new \BadMethodCallException("Expected at least one argument.");
return $this->find_one($key);
}

if (count($args) == 1) {
$key = $args[0];

if (!is_array($key)) {
return $this->find_one($key);
}

$args = $key;
}

return $this->find_many($args);
return $this->find_many($keys);
}

/**
* Finds one records.
*
* @param TKey $key
* @param int|non-empty-string $key
*
* @return ActiveRecord<TValue>
* @return ActiveRecord&TValue
*/
private function find_one($key): ActiveRecord
private function find_one(int|string $key): ActiveRecord
{
$record = $this->activerecord_cache->retrieve($key);

if ($record) {
return $record;
}
assert(is_string($this->primary));

/** @var TValue|false $record */
$record = $this->where([ $this->primary => $key ])->one;

if (!$record) {
throw new RecordNotFound(
"Record <q>{$key}</q> does not exist in model <q>{$this->activerecord_class}</q>.",
"No $this->activerecord_class for key $key",
[ $key => null ]
);
}

$this->activerecord_cache->store($record);

return $record;
}

Expand All @@ -176,18 +116,7 @@ private function find_one($key): ActiveRecord
*/
private function find_many(array $keys): array
{
$records = $missing = array_fill_keys($keys, null);

foreach ($keys as $key) {
$record = $this->activerecord_cache->retrieve($key);

if (!$record) {
continue;
}

$records[$key] = $record;
unset($missing[$key]);
}
$records = $missing = array_fill_keys($keys, value: null);

if ($missing) {
$primary = $this->primary;
Expand All @@ -200,8 +129,6 @@ private function find_many(array $keys): array
$key = $record->$primary;
$records[$key] = $record;
unset($missing[$key]);

$this->activerecord_cache->store($record);
}
}

Expand All @@ -210,11 +137,7 @@ private function find_many(array $keys): array
if ($missing) {
if (count($missing) > 1) {
throw new RecordNotFound(
sprintf(
"Records `%s` do not exist for `%s`",
implode('`, `', array_keys($missing)),
$this->activerecord_class
),
"No $this->activerecord_class for keys " . implode(', ', array_keys($missing)),
$records
);
}
Expand All @@ -223,7 +146,7 @@ private function find_many(array $keys): array
$key = array_shift($key);

throw new RecordNotFound(
"Record `$key` does not exist for `$this->activerecord_class`",
"No $this->activerecord_class for key $key",
$records
);
}
Expand All @@ -244,38 +167,13 @@ public function query(): Query
/**
* Returns a new query with the WHERE clause initialized with the provided conditions and arguments.
*
* @param ...$conditions_and_args
*
* @return Query<TValue>
*/
public function where(...$conditions_and_args): Query
{
return $this->query()->where(...$conditions_and_args);
}

/**
* Because records are cached, we need to remove the record from the cache when it is saved,
* so that loading the record again returns the updated record, not the one in the cache.
*/
public function save(array $values, mixed $id = null, array $options = []): mixed
{
if ($id) {
$this->activerecord_cache->eliminate($id);
}

return parent::save($values, $id, $options);
}

/**
* Eliminates the record from the cache.
*
* @inheritdoc
* @see Query::where()
*/
public function delete($key)
public function where(mixed ...$conditions_and_args): Query
{
$this->activerecord_cache->eliminate($key);

return parent::delete($key);
return $this->query()->where(...$conditions_and_args);
}

/**
Expand Down
21 changes: 21 additions & 0 deletions lib/ActiveRecord/ModelProviderWithClosure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace ICanBoogie\ActiveRecord;

use Closure;

/**
* Creates a {@see ModelProvider} from a closure.
*/
final readonly class ModelProviderWithClosure implements ModelProvider
{
public function __construct(
private Closure $closure
) {
}

public function model_for_record(string $activerecord_class): Model
{
return ($this->closure)($activerecord_class);
}
}
Loading

0 comments on commit 2a4d60d

Please sign in to comment.