Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generated fields #462

Merged
merged 11 commits into from
Feb 8, 2024
Merged
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
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
Loading