Issues (19)

src/OptimisticLock.php (2 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\ORM\Entity\Behavior;
6
7
use Cycle\ORM\Entity\Behavior\Schema\BaseModifier;
8
use Cycle\ORM\Entity\Behavior\Schema\RegistryModifier;
9
use Cycle\ORM\Entity\Behavior\Exception\BehaviorCompilationException;
10
use Cycle\ORM\Entity\Behavior\Listener\OptimisticLock as Listener;
11
use Cycle\ORM\Schema\GeneratedField;
12
use Cycle\Schema\Definition\Field;
13
use Cycle\Schema\Registry;
14
use Doctrine\Common\Annotations\Annotation\Enum;
15
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
16
use Doctrine\Common\Annotations\Annotation\Target;
17
use JetBrains\PhpStorm\ArrayShape;
18
use JetBrains\PhpStorm\ExpectedValues;
19
20
/**
21
 * Implements the Optimistic Lock strategy.
22
 * Used to prevent concurrent editing of a record in the database. When an entity is locked, the transaction is aborted.
23
 * Please keep in mind, the behavior wraps the command in a special WrappedCommand wrapper.
24
 * The behavior has three parameters:
25
 *    - field - is a property with the version in the entity
26
 *    - column - is a column in the database.
27
 *    - rule - the strategy for storing the version of the entity
28
 * Rule can be one of several rules (class constants can be used):
29
 *    - RULE_MICROTIME - string with microtime value
30
 *    - RULE_RAND_STR - random string
31
 *    - RULE_INCREMENT - automatically incrementing integer version
32
 *    - RULE_DATETIME - datetime of the entity version
33
 *    - RULE_MANUAL - manually configured rule
34
 * The MANUAL rule provides for the completely manual configuration of an entity property and entity versioning.
35
 *
36
 * @Annotation
37
 * @NamedArgumentConstructor()
38
 * @Target({"CLASS"})
39
 */
40
#[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor]
41
final class OptimisticLock extends BaseModifier
42
{
43
    public const RULE_MICROTIME = Listener::RULE_MICROTIME;
44
    public const RULE_RAND_STR = Listener::RULE_RAND_STR;
45
    public const RULE_INCREMENT = Listener::RULE_INCREMENT;
46
    public const RULE_DATETIME = Listener::RULE_DATETIME;
47
    public const RULE_MANUAL = Listener::RULE_MANUAL;
48
    private const DEFAULT_INT_VERSION = 1;
49
    private const STRING_COLUMN_LENGTH = 32;
50
51
    private ?string $column = null;
52
53
    /**
54
     * @param non-empty-string $field Version property name
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
55
     * @param non-empty-string|null $column Version column name
56
     * @param non-empty-string|null $rule
57
     */
58
    public function __construct(
59 40
        private string $field = 'version',
60
        ?string $column = null,
61
        /** @Enum({"microtime", "random-string", "increment", "datetime"}) */
62
        #[ExpectedValues(valuesFromClass: self::class)]
63
        private ?string $rule = null,
64
    ) {
65
        $this->column = $column;
66 40
    }
67
68
    public function compute(Registry $registry): void
69 40
    {
70
        $modifier = new RegistryModifier($registry, $this->role);
71 40
        $this->column = $modifier->findColumnName($this->field, $this->column);
72
73
        if ($this->column !== null) {
74 40
            $this->addField($registry);
75
        }
76
    }
77
78 40
    public function render(Registry $registry): void
79 40
    {
80
        $this->column = (new RegistryModifier($registry, $this->role))
81
            ->findColumnName($this->field, $this->column)
82
            ?? $this->field;
83 40
84
        $this->addField($registry);
85 40
    }
86 40
87
    protected function getListenerClass(): string
88 40
    {
89 40
        return Listener::class;
90
    }
91
92
    #[ArrayShape(['field' => 'string', 'rule' => 'null|string'])]
93 40
    protected function getListenerArgs(): array
94
    {
95 40
        return [
96 40
            'field' => $this->field,
97 40
            'rule' => $this->rule,
98
        ];
99 40
    }
100
101
    /**
102
     * Compute rule based on column type
103
     *
104
     * @return non-empty-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
105
     *
106
     * @throws BehaviorCompilationException
107
     */
108
    private function computeRule(Field $field): string
109 40
    {
110
        $type = $field->getType();
111 40
112
        return match (true) {
113
            RegistryModifier::isIntegerType($type) => self::RULE_INCREMENT,
114
            RegistryModifier::isStringType($type) => self::RULE_MICROTIME,
115 40
            RegistryModifier::isDatetimeType($type) => self::RULE_DATETIME,
116
            default => throw new BehaviorCompilationException('Failed to compute rule based on column type.'),
117
        };
118
    }
119 40
120
    private function addField(Registry $registry): void
121 40
    {
122
        $fields = $registry->getEntity($this->role)->getFields();
123
124
        \assert($this->column !== null);
125 40
126 40
        $this->rule ??= $fields->has($this->field)
127
            ? $this->computeRule($fields->get($this->field))
128 40
            // rule not set, field not fount
129
            : Listener::DEFAULT_RULE;
130 40
131
        $modifier = new RegistryModifier($registry, $this->role);
132 40
133 40
        switch ($this->rule) {
134 40
            case self::RULE_INCREMENT:
135
                $modifier
136
                    ->addIntegerColumn(
137 40
                        $this->column,
138 40
                        $this->field,
139 40
                        GeneratedField::BEFORE_INSERT | GeneratedField::BEFORE_UPDATE,
140 40
                    )
141
                    ->nullable(false)
142
                    ->defaultValue(self::DEFAULT_INT_VERSION);
143 40
                break;
144 40
            case self::RULE_RAND_STR:
145 40
            case self::RULE_MICROTIME:
146 40
                $modifier
147
                    ->addStringColumn(
148
                        $this->column,
149
                        $this->field,
150
                        GeneratedField::BEFORE_INSERT | GeneratedField::BEFORE_UPDATE,
151
                    )
152
                    ->nullable(false)
153
                    ->string(self::STRING_COLUMN_LENGTH);
154
                break;
155
            case self::RULE_DATETIME:
156
                $modifier->addDatetimeColumn(
157
                    $this->column,
158
                    $this->field,
159
                    GeneratedField::BEFORE_INSERT | GeneratedField::BEFORE_UPDATE,
160
                );
161
                break;
162
            default:
163
                throw new BehaviorCompilationException(
164
                    \sprintf(
165
                        'Wrong rule `%s` for the %s behavior in the `%s.%s` field.',
166
                        $this->rule,
167
                        self::class,
168
                        $this->role,
169
                        $this->field,
170
                    ),
171
                );
172
        }
173
    }
174
}
175