Skip to content

Commit

Permalink
Merge pull request #462: Add Generated Fields option into ORM Schema
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk authored Feb 8, 2024
2 parents c10b995 + 2e662dd commit 31e29e4
Show file tree
Hide file tree
Showing 31 changed files with 705 additions and 48 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# CHANGELOG

v2.6.0 (22.12.2023)
v2.7.0 (08.02.2024)
--------------------
- Add Generated Fields option into ORM Schema by @roxblnfk (#462)

v2.6.1 (04.01.2024)
--------------------
- Fix compatibility with PHP 8.3 by @msmakouz (#454)

- v2.6.0 (22.12.2023)
--------------------
- Add support for `loophp/collection` v7 by @msmakouz (#448)
- Fix wrong adding table prefix on joins by @msmakouz (#447)
Expand Down
5 changes: 2 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"require": {
"php": ">=8.0",
"ext-pdo": "*",
"cycle/database": "^2.6",
"cycle/database": "^2.8.1",
"doctrine/instantiator": "^1.3.1 || ^2.0",
"spiral/core": "^2.8 || ^3.0"
},
Expand All @@ -51,8 +51,7 @@
"phpunit/phpunit": "^9.5",
"ramsey/uuid": "^4.0",
"spiral/tokenizer": "^2.8 || ^3.0",
"vimeo/psalm": "5.21",
"buggregator/trap": "^1.4"
"vimeo/psalm": "5.21"
},
"autoload": {
"psr-4": {
Expand Down
93 changes: 82 additions & 11 deletions src/Command/Database/Insert.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Cycle\ORM\Command\Traits\MapperTrait;
use Cycle\ORM\Heap\State;
use Cycle\ORM\MapperInterface;
use Cycle\ORM\Schema\GeneratedField;

/**
* Insert data into associated table and provide lastInsertID promise.
Expand All @@ -20,14 +21,20 @@ final class Insert extends StoreCommand
use ErrorTrait;
use MapperTrait;

/**
* @param non-empty-string $table
* @param string[] $primaryKeys
* @param non-empty-string|null $pkColumn
* @param array<non-empty-string, int> $generated
*/
public function __construct(
DatabaseInterface $db,
string $table,
State $state,
?MapperInterface $mapper,
/** @var string[] */
private array $primaryKeys = [],
private ?string $pkColumn = null
private ?string $pkColumn = null,
private array $generated = [],
) {
parent::__construct($db, $table, $state);
$this->mapper = $mapper;
Expand All @@ -40,7 +47,12 @@ public function isReady(): bool

public function hasData(): bool
{
return $this->columns !== [] || $this->state->getData() !== [];
return match (true) {
$this->columns !== [],
$this->state->getData() !== [],
$this->hasGeneratedFields() => true,
default => false,
};
}

public function getStoreData(): array
Expand All @@ -59,6 +71,7 @@ public function getStoreData(): array
public function execute(): void
{
$state = $this->state;
$returningFields = [];

if ($this->appendix !== []) {
$state->setData($this->appendix);
Expand All @@ -72,25 +85,59 @@ public function execute(): void
unset($uncasted[$key]);
}
}
// unset db-generated fields if they are null
foreach ($this->generated as $column => $mode) {
if (($mode & GeneratedField::ON_INSERT) === 0x0) {
continue;
}

$returningFields[$column] = $mode;
if (!isset($uncasted[$column])) {
unset($uncasted[$column]);
}
}
$uncasted = $this->prepareData($uncasted);

$insert = $this->db
->insert($this->table)
->values(\array_merge($this->columns, $uncasted));

if ($this->pkColumn !== null && $insert instanceof ReturningInterface) {
$insert->returning($this->pkColumn);
if ($this->pkColumn !== null && $returningFields === []) {
$returningFields[$this->primaryKeys[0]] ??= $this->pkColumn;
}

$insertID = $insert->run();
if ($insert instanceof ReturningInterface && $returningFields !== []) {
// Map generated fields to columns
$returning = $this->mapper->mapColumns($returningFields);
// Array of [field name => column name]
$returning = \array_combine(\array_keys($returningFields), \array_keys($returning));

if ($insertID !== null && \count($this->primaryKeys) === 1) {
$fpk = $this->primaryKeys[0]; // first PK
if (!isset($data[$fpk])) {
$insert->returning(...\array_values($returning));

$insertID = $insert->run();

if (\count($returning) === 1) {
$field = \array_key_first($returning);
$state->register(
$fpk,
$this->mapper === null ? $insertID : $this->mapper->cast([$fpk => $insertID])[$fpk]
$field,
$this->mapper === null ? $insertID : $this->mapper->cast([$field => $insertID])[$field],
);
} else {
foreach ($this->mapper->cast($insertID) as $field => $value) {
$state->register($field, $value);
}
}
} else {
$insertID = $insert->run();

if ($insertID !== null && \count($this->primaryKeys) === 1) {
$fpk = $this->primaryKeys[0]; // first PK
if (!isset($data[$fpk])) {
$state->register(
$fpk,
$this->mapper === null ? $insertID : $this->mapper->cast([$fpk => $insertID])[$fpk]
);
}
}
}

Expand All @@ -103,4 +150,28 @@ public function register(string $key, mixed $value): void
{
$this->state->register($key, $value);
}

/**
* Has fields that weren't provided but will be generated by the database or PHP.
*/
private function hasGeneratedFields(): bool
{
if ($this->generated === []) {
return false;
}

$data = $this->state->getData();

foreach ($this->generated as $field => $mode) {
if (($mode & (GeneratedField::ON_INSERT | GeneratedField::BEFORE_INSERT)) === 0x0) {
continue;
}

if (!isset($data[$field])) {
return true;
}
}

return false;
}
}
4 changes: 2 additions & 2 deletions src/Heap/State.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,15 @@ public function getChanges(): array

public function getValue(string $key): mixed
{
return array_key_exists($key, $this->data) ? $this->data[$key] : ($this->transactionData[$key] ?? null);
return \array_key_exists($key, $this->data) ? $this->data[$key] : ($this->transactionData[$key] ?? null);
}

public function hasValue(string $key, bool $allowNull = true): bool
{
if (!$allowNull) {
return isset($this->data[$key]) || isset($this->transactionData[$key]);
}
return array_key_exists($key, $this->data) || array_key_exists($key, $this->transactionData);
return \array_key_exists($key, $this->data) || \array_key_exists($key, $this->transactionData);
}

public function register(string $key, mixed $value): void
Expand Down
2 changes: 1 addition & 1 deletion src/Heap/Traits/WaitFieldTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function waitField(string $key, bool $required = true): void

public function getWaitingFields(bool $requiredOnly = false): array
{
return array_keys($requiredOnly ? array_filter($this->waitingFields) : $this->waitingFields);
return \array_keys($requiredOnly ? \array_filter($this->waitingFields) : $this->waitingFields);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/Mapper/DatabaseMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ abstract class DatabaseMapper implements MapperInterface
protected array $primaryKeys;
private ?TypecastInterface $typecast;
protected RelationMap $relationMap;
/** @var array<non-empty-string, int> */
private array $generatedFields;

public function __construct(
ORMInterface $orm,
Expand All @@ -53,6 +55,7 @@ public function __construct(
$this->columns[\is_int($property) ? $column : $property] = $column;
}

$this->generatedFields = $schema->define($role, SchemaInterface::GENERATED_FIELDS) ?? [];
// Parent's fields
$parent = $schema->define($role, SchemaInterface::PARENT);
while ($parent !== null) {
Expand Down Expand Up @@ -128,6 +131,7 @@ public function queueCreate(object $entity, Node $node, State $state): CommandIn
$this,
$this->primaryKeys,
\count($this->primaryColumns) === 1 ? $this->primaryColumns[0] : null,
$this->generatedFields,
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Relation/BelongsTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ private function checkNullValuePossibility(Tuple $tuple): bool
}

if ($tuple->status < Tuple::STATUS_PREPROCESSED
&& array_intersect($this->innerKeys, $tuple->state->getWaitingFields(false)) !== []
&& \array_intersect($this->innerKeys, $tuple->state->getWaitingFields(false)) !== []
) {
return true;
}
Expand Down
2 changes: 0 additions & 2 deletions src/Relation/ManyToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,6 @@ protected function newLink(Pool $pool, Tuple $tuple, PivotedStorage $storage, ob

foreach ($this->throughInnerKeys as $i => $pInnerKey) {
$pTuple->state->register($pInnerKey, $tuple->state->getTransactionData()[$this->innerKeys[$i]] ?? null);

// $rState->forward($this->outerKeys[$i], $pState, $this->throughOuterKeys[$i]);
}

if ($this->inversion === null) {
Expand Down
2 changes: 1 addition & 1 deletion src/Relation/RefersTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public function queue(Pool $pool, Tuple $tuple): void
if ($rTuple->status === Tuple::STATUS_PROCESSED
|| ($rTuple->status > Tuple::STATUS_PREPARING
&& $rTuple->state->getStatus() !== node::NEW
&& array_intersect($this->outerKeys, $rTuple->state->getWaitingFields()) === [])
&& \array_intersect($this->outerKeys, $rTuple->state->getWaitingFields()) === [])
) {
$this->pullValues($tuple->state, $rTuple->state);
$node->setRelation($this->getName(), $related);
Expand Down
38 changes: 38 additions & 0 deletions src/Schema/GeneratedField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Schema;

/**
* Values for {@see SchemaInterface::GENERATED_FIELDS}
*/
final class GeneratedField
{
/**
* Field value is generated in the user space before transaction running.
*/
public const DEFAULT = 0;

/**
* Field value is generated by PHP code before Insert command running
* like with `CreatedAt` or `Uuid` Entity Behaviors
*
* @link https://github.com/cycle/entity-behavior
* @link https://github.com/cycle/entity-behavior-uuid
*/
public const BEFORE_INSERT = 1;

/**
* Field value is generated by the Database.
* It is: autoincrement fields, sequences, timestamps, etc.
*/
public const ON_INSERT = 2;

/**
* Field value is generated by PHP code before Update command running like with `UpdatedAt` Entity Behavior.
*
* @link https://github.com/cycle/entity-behavior
*/
public const BEFORE_UPDATE = 4;
}
1 change: 1 addition & 0 deletions src/SchemaInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface SchemaInterface
public const DISCRIMINATOR = 17; // Discriminator column name for single table inheritance
public const LISTENERS = 18;
public const TYPECAST_HANDLER = 19; // Typecast handler definition that implements TypecastInterface
public const GENERATED_FIELDS = 20; // List of generated fields [field => generating type]

/**
* Return all roles defined within the schema.
Expand Down
8 changes: 4 additions & 4 deletions src/Transaction/Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public function complete(): void
{
if ($this->mode === self::MODE_OPEN_TRANSACTION) {
// Commit all of the open and normalized database transactions
foreach (array_reverse($this->drivers) as $driver) {
foreach (\array_reverse($this->drivers) as $driver) {
/** @var DriverInterface $driver */
$driver->commitTransaction();
}
Expand All @@ -94,14 +94,14 @@ public function rollback(): void
{
if ($this->mode === self::MODE_OPEN_TRANSACTION) {
// Close all open and normalized database transactions
foreach (array_reverse($this->drivers) as $driver) {
foreach (\array_reverse($this->drivers) as $driver) {
/** @var DriverInterface $driver */
$driver->rollbackTransaction();
}
}

// Close all of external types of transactions (revert changes)
foreach (array_reverse($this->executed) as $command) {
foreach (\array_reverse($this->executed) as $command) {
if ($command instanceof RollbackMethodInterface) {
$command->rollBack();
}
Expand Down Expand Up @@ -145,7 +145,7 @@ private function prepareTransaction(DriverInterface $driver): void

if ($this->mode === self::MODE_CONTINUE_TRANSACTION) {
if ($driver->getTransactionLevel() === 0) {
throw new RunnerException(sprintf(
throw new RunnerException(\sprintf(
'The `%s` driver connection has no opened transaction.',
$driver->getType()
));
Expand Down
Loading

0 comments on commit 31e29e4

Please sign in to comment.