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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"require": {
"php": ">=8.1",
"cycle/orm": "^2.18",
"cycle/database": "^2.20",
"cycle/database": "^2.23",
"yiisoft/friendly-exception": "^1.1"
},
"require-dev": {
Expand Down
12 changes: 0 additions & 12 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -370,32 +370,20 @@
<code><![CDATA[public function getIterator(): \Traversable]]></code>
</MissingOverrideAttribute>
<MixedArrayAccess>
<code><![CDATA[$association['database']]]></code>
<code><![CDATA[$association['schema']]]></code>
<code><![CDATA[$association['table']]]></code>
<code><![CDATA[$this->relations[$entity][$name]]]></code>
<code><![CDATA[$this->tables[$entity]['database']]]></code>
<code><![CDATA[$this->tables[$entity]['schema']]]></code>
<code><![CDATA[$this->tables[$entity]['table']]]></code>
</MixedArrayAccess>
<MixedArrayAssignment>
<code><![CDATA[$children[]]]></code>
<code><![CDATA[$relations[$name]]]></code>
</MixedArrayAssignment>
<MixedAssignment>
<code><![CDATA[$association]]></code>
<code><![CDATA[$children]]></code>
<code><![CDATA[$relations]]></code>
<code><![CDATA[$schema]]></code>
<code><![CDATA[$schema]]></code>
</MixedAssignment>
<MixedReturnStatement>
<code><![CDATA[$this->children[$entity]]]></code>
<code><![CDATA[$this->relations[$entity]]]></code>
<code><![CDATA[$this->relations[$entity][$name]]]></code>
<code><![CDATA[$this->tables[$entity]['database']]]></code>
<code><![CDATA[$this->tables[$entity]['schema']]]></code>
<code><![CDATA[$this->tables[$entity]['table']]]></code>
</MixedReturnStatement>
<PossiblyUnusedMethod>
<code><![CDATA[__construct]]></code>
Expand Down
140 changes: 103 additions & 37 deletions src/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@ final class Registry implements \IteratorAggregate
private array $entities = [];

private DatabaseProviderInterface $dbal;

/**
* @var \SplObjectStorage<
* Entity,
* array{database: string, table: non-empty-string, schema: AbstractTable|null}|null
* >
*/
private \SplObjectStorage $tables;

private \SplObjectStorage $children;
private \SplObjectStorage $relations;
private Defaults $defaults;
Expand Down Expand Up @@ -143,33 +151,12 @@ public function linkTable(Entity $entity, ?string $database, string $table): sel

$database = $this->dbal->database($database)->getName();

$schema = null;
foreach ($this->tables as $other) {
$association = $this->tables[$other];

if ($association === null) {
continue;
}

// avoid schema duplication
if ($association['database'] === $database && $association['table'] === $table) {
$schema = $association['schema'];
break;
}
}

if ($schema === null) {
$dbTable = $this->dbal->database($database)->table($table);
if (!\method_exists($dbTable, 'getSchema')) {
throw new RegistryException('Unable to retrieve table schema.');
}
$schema = $dbTable->getSchema();
}

// Table schemas are loaded lazily and in bulk: by the time the first schema is requested
// all the linked tables are known, so the whole set costs a constant number of queries.
$this->tables[$entity] = [
'database' => $database,
'table' => $table,
'schema' => $schema,
'schema' => null,
];

return $this;
Expand All @@ -192,11 +179,7 @@ public function hasTable(Entity $entity): bool
*/
public function getDatabase(Entity $entity): string
{
if (!$this->hasTable($entity)) {
throw new RegistryException("Entity `{$entity->getRole()}` has no assigned table");
}

return $this->tables[$entity]['database'];
return $this->getTableAssociation($entity)['database'];
}

/**
Expand All @@ -206,23 +189,23 @@ public function getDatabase(Entity $entity): string
*/
public function getTable(Entity $entity): string
{
if (!$this->hasTable($entity)) {
throw new RegistryException("Entity `{$entity->getRole()}` has no assigned table");
}

return $this->tables[$entity]['table'];
return $this->getTableAssociation($entity)['table'];
}

/**
* @throws RegistryException
*/
public function getTableSchema(Entity $entity): AbstractTable
{
if (!$this->hasTable($entity)) {
throw new RegistryException("Entity `{$entity->getRole()}` has no assigned table");
$schema = $this->getTableAssociation($entity)['schema'];

if ($schema === null) {
$this->loadTableSchemas();
$schema = $this->getTableAssociation($entity)['schema'];
\assert($schema !== null);
}

return $this->tables[$entity]['schema'];
return $schema;
}

/**
Expand Down Expand Up @@ -286,4 +269,87 @@ protected function hasInstance(Entity $entity): bool
{
return array_search($entity, $this->entities, true) !== false;
}

/**
* @return array{database: string, table: non-empty-string, schema: AbstractTable|null}
*
* @throws RegistryException
*/
private function getTableAssociation(Entity $entity): array
{
if (!$this->hasInstance($entity)) {
throw new RegistryException("Undefined entity `{$entity->getRole()}`");
}

$association = $this->tables[$entity];

if ($association === null) {
throw new RegistryException("Entity `{$entity->getRole()}` has no assigned table");
}

return $association;
}

/**
* Load schemas for all the linked tables that don't have one yet. Entities sharing the same
* database and table receive the same {@see AbstractTable} instance.
*
* @throws RegistryException
* @throws DBALException
*/
private function loadTableSchemas(): void
{
/** @var array<string, array<non-empty-string, AbstractTable>> $loaded */
$loaded = [];
/** @var array<string, array<non-empty-string, true>> $pending */
$pending = [];
foreach ($this->tables as $entity) {
$association = $this->tables[$entity];
if ($association === null) {
continue;
}

if ($association['schema'] !== null) {
$loaded[$association['database']][$association['table']] = $association['schema'];
} else {
$pending[$association['database']][$association['table']] = true;
}
}

foreach ($pending as $database => $tables) {
// avoid schema duplication
$names = \array_keys(\array_diff_key($tables, $loaded[$database] ?? []));
if ($names === []) {
continue;
}

$db = $this->dbal->database($database);
if (\method_exists($db, 'getSchemas')) {
/** @var array<non-empty-string, AbstractTable> $schemas */
$schemas = $db->getSchemas($names);
$loaded[$database] = ($loaded[$database] ?? []) + $schemas;
continue;
}

foreach ($names as $name) {
$dbTable = $db->table($name);
if (!\method_exists($dbTable, 'getSchema')) {
throw new RegistryException('Unable to retrieve table schema.');
}
/** @var AbstractTable $schema */
$schema = $dbTable->getSchema();
$loaded[$database][$name] = $schema;
}
}

foreach ($this->tables as $entity) {
$association = $this->tables[$entity];
if ($association === null || $association['schema'] !== null) {
continue;
}

$association['schema'] = $loaded[$association['database']][$association['table']];
$this->tables[$entity] = $association;
}
}
}
126 changes: 126 additions & 0 deletions tests/Schema/Fixtures/LegacyDatabase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

namespace Cycle\Schema\Tests\Fixtures;

use Cycle\Database\DatabaseInterface;
use Cycle\Database\DatabaseProviderInterface;
use Cycle\Database\Driver\DriverInterface;
use Cycle\Database\Query\DeleteQuery;
use Cycle\Database\Query\InsertQuery;
use Cycle\Database\Query\SelectQuery;
use Cycle\Database\Query\UpdateQuery;
use Cycle\Database\StatementInterface;
use Cycle\Database\TableInterface;

/**
* Emulates a cycle/database version without the bulk `getSchemas()` method to exercise the
* per-table fallback of the Registry. Only the methods the Registry touches are functional.
*/
class LegacyDatabase implements DatabaseInterface, DatabaseProviderInterface
{
public function __construct(
private DatabaseInterface $database,
private bool $bareTables = false,
) {}

public function database(?string $database = null): DatabaseInterface
{
return $this;
}

public function getName(): string
{
return $this->database->getName();
}

public function table(string $name): TableInterface
{
if ($this->bareTables) {
return new LegacyTable($this->database->table($name));
}

return $this->database->table($name);
}

public function getType(): string
{
return $this->database->getType();
}

public function getDriver(int $type = self::WRITE): DriverInterface
{
return $this->database->getDriver($type);
}

public function withPrefix(string $prefix, bool $add = true): DatabaseInterface
{
throw new \BadMethodCallException(__METHOD__ . ' is not expected to be called');
}

public function getPrefix(): string
{
return $this->database->getPrefix();
}

public function hasTable(string $name): bool
{
return $this->database->hasTable($name);
}

public function getTables(): array
{
return $this->database->getTables();
}

public function execute(string $query, array $parameters = []): int
{
return $this->database->execute($query, $parameters);
}

public function query(string $query, array $parameters = []): StatementInterface
{
return $this->database->query($query, $parameters);
}

public function insert(string $table = ''): InsertQuery
{
return $this->database->insert($table);
}

public function update(string $table = '', array $values = [], array $where = []): UpdateQuery
{
return $this->database->update($table, $values, $where);
}

public function delete(string $table = '', array $where = []): DeleteQuery
{
return $this->database->delete($table, $where);
}

public function select(mixed $columns = '*'): SelectQuery
{
return $this->database->select($columns);
}

public function transaction(callable $callback, ?string $isolationLevel = null): mixed
{
return $this->database->transaction($callback, $isolationLevel);
}

public function begin(?string $isolationLevel = null): bool
{
return $this->database->begin($isolationLevel);
}

public function commit(): bool
{
return $this->database->commit();
}

public function rollback(): bool
{
return $this->database->rollback();
}
}
Loading
Loading