• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ICanBoogie / ActiveRecord / 11644487095

02 Nov 2024 05:11PM UTC coverage: 84.34% (+0.6%) from 83.771%
11644487095

push

github

olvlvl
Test binary and blob columns

1411 of 1673 relevant lines covered (84.34%)

21.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.74
/lib/ActiveRecord.php
1
<?php
2

3
namespace ICanBoogie;
4

5
use ICanBoogie\ActiveRecord\Model;
6
use ICanBoogie\ActiveRecord\Query;
7
use ICanBoogie\ActiveRecord\RecordNotValid;
8
use ICanBoogie\ActiveRecord\Schema;
9
use ICanBoogie\ActiveRecord\StaticModelProvider;
10
use ICanBoogie\Validate\ValidationErrors;
11
use LogicException;
12
use ReflectionException;
13
use Throwable;
14

15
use function array_keys;
16
use function is_array;
17
use function is_numeric;
18

19
/**
20
 * Active Record facilitates the creation and use of business objects whose data require persistent
21
 * storage via a database.
22
 *
23
 * @method ValidationErrors validate() Validate the active record, returns an array of errors.
24
 *
25
 * @property-read Model $model The model managing the active record.
26
 * @uses self::get_model()
27
 * @property-read bool $is_new Whether the record is new or not.
28
 * @uses self::get_is_new()
29
 *
30
 * @template TKey of int|string|string[]
31
 */
32
abstract class ActiveRecord extends Prototyped
33
{
34
    public const SAVE_SKIP_VALIDATION = 'skip_validation';
35

36
    /**
37
     * Returns a new query.
38
     *
39
     * @return Query<static>
40
     *
41
     * @see Model::query()
42
     */
43
    final public static function query(): Query
44
    {
45
        return StaticModelProvider::model_for_record(static::class)->query();
2✔
46
    }
47

48
    /**
49
     * Returns a new query with the WHERE clause initialized with the provided conditions and arguments.
50
     *
51
     * @param mixed ...$conditions_and_args
52
     *
53
     * @return Query<static>
54
     *
55
     * @see Query::where()
56
     */
57
    final public static function where(...$conditions_and_args): Query
58
    {
59
        return self::query()->where(...$conditions_and_args);
1✔
60
    }
61

62
    /**
63
     * Model managing the active record.
64
     *
65
     * @var Model<TKey, static>
66
     */
67
    private Model $model;
68

69
    /**
70
     * @return Model<TKey, static>
71
     */
72
    protected function get_model(): Model
73
    {
74
        return $this->model
8✔
75
            ??= StaticModelProvider::model_for_record($this::class);
8✔
76
    }
77

78
    /**
79
     * @param ?Model<TKey,static> $model
80
     *     The model managing the active record. A {@link Model} instance can be specified as well as a model
81
     *     identifier. If `$model` is null, the model will be resolved with {@link StaticModelProvider} when required.
82
     */
83
    public function __construct(Model $model = null)
84
    {
85
        if ($model) {
32✔
86
            $this->model = $model;
24✔
87
        }
88
    }
89

90
    /**
91
     * Removes the {@link $model} property.
92
     *
93
     * Properties whose value are instances of the {@link ActiveRecord} class are removed from the
94
     * exported properties.
95
     *
96
     * @return array<non-empty-string, mixed>
97
     *
98
     * @throws ReflectionException
99
     */
100
    public function __sleep() // @phpstan-ignore-line
101
    {
102
        $properties = parent::__sleep();
2✔
103

104
        /** @phpstan-ignore-next-line */
105
        unset($properties['model']);
2✔
106

107
        foreach (array_keys($properties) as $property) {
2✔
108
            if ($this->$property instanceof self) {
×
109
                unset($properties[$property]);
×
110
            }
111
        }
112

113
        return $properties;
2✔
114
    }
115

116
    /**
117
     * Removes `model` from the output.
118
     *
119
     * @return array<non-empty-string, mixed>
120
     */
121
    public function __debugInfo(): array
122
    {
123
        $array = (array)$this;
1✔
124

125
        unset($array["\0" . __CLASS__ . "\0model"]);
1✔
126

127
        return $array;
1✔
128
    }
129

130
    /**
131
     * Whether the record is new or not.
132
     */
133
    protected function get_is_new(): bool
134
    {
135
        $primary = $this->get_model()->primary;
×
136

137
        if (is_array($primary)) {
×
138
            foreach ($primary as $property) {
×
139
                if (empty($this->$property)) {
×
140
                    return true;
×
141
                }
142
            }
143
        } elseif (empty($this->$primary)) {
×
144
            return true;
×
145
        }
146

147
        return false;
×
148
    }
149

150
    /**
151
     * Saves the active record using its model.
152
     *
153
     * @param array<string, mixed> $options Save options.
154
     *
155
     * @return bool|int|mixed Primary key value of the active record, or a boolean if the primary key
156
     * is not a serial.
157
     *
158
     * @throws Throwable
159
     */
160
    public function save(array $options = []): mixed
161
    {
162
        if (empty($options[self::SAVE_SKIP_VALIDATION])) {
4✔
163
            $this->assert_is_valid();
4✔
164
        }
165

166
        $model = $this->get_model();
3✔
167
        $schema = $model->extended_schema;
3✔
168
        $properties = $this->alter_persistent_properties($this->to_array(), $schema);
3✔
169

170
        #
171
        # Multi-column primary key
172
        #
173

174
        $primary = $model->primary;
3✔
175

176
        if (is_array($primary)) {
3✔
177
            return $model->insert($properties, [ 'on duplicate' => true ]);
×
178
        }
179

180
        #
181
        # Non auto-increment primary key, unless the key is inherited from parent model.
182
        #
183

184
        if (
185
            !$model->parent && $primary && isset($properties[$primary])
3✔
186
            && !$model->extended_schema->columns[$primary]->auto_increment
3✔
187
        ) {
188
            return $model->insert($properties, [ 'on duplicate' => true ]);
×
189
        }
190

191
        #
192
        # Serial primary key
193
        #
194

195
        $id = null;
3✔
196

197
        if (isset($properties[$primary])) {
3✔
198
            $id = $properties[$primary];
×
199
            unset($properties[$primary]);
×
200
            assert(is_numeric($id));
201
        }
202

203
        // @phpstan-ignore-next-line
204
        $rc = $model->save($properties, $id);
3✔
205

206
        if ($id === null) {
3✔
207
            $this->$primary = $rc;
3✔
208
        }
209

210
        return (int) $rc;
3✔
211
    }
212

213
    /**
214
     * Assert that a record is valid.
215
     *
216
     * @throws RecordNotValid if the record is not valid.
217
     */
218
    public function assert_is_valid(): void
219
    {
220
        $errors = $this->validate();
4✔
221

222
        if (count($errors)) {
4✔
223
            throw new RecordNotValid($this, $errors);
1✔
224
        }
225
    }
226

227
    /**
228
     * Creates validation rules.
229
     *
230
     * @return array<string, mixed>
231
     */
232
    public function create_validation_rules(): array
233
    {
234
        return [];
7✔
235
    }
236

237
    /**
238
     * Unless it's an acceptable value for a column, columns with `null` values are discarded.
239
     * This way, we don't have to define every property before saving our active record.
240
     *
241
     * @param array<non-empty-string, mixed> $properties
242
     * @param Schema $schema The model's extended schema.
243
     *
244
     * @return array<non-empty-string, mixed> The altered persistent properties
245
     */
246
    protected function alter_persistent_properties(array $properties, Schema $schema): array
247
    {
248
        foreach ($properties as $identifier => $value) {
3✔
249
            if ($value !== null || ($schema->has_column($identifier) && $schema->columns[$identifier]->null)) {
3✔
250
                continue;
3✔
251
            }
252

253
            unset($properties[$identifier]);
×
254
        }
255

256
        return $properties;
3✔
257
    }
258

259
    /**
260
     * Updates primary key.
261
     *
262
     * @param int|non-empty-string|non-empty-array<non-empty-string> $primary_key
263
     */
264
    protected function update_primary_key(int|array|string $primary_key): void
265
    {
266
        $model = $this->get_model();
×
267
        $model_class = $model::class;
×
268
        $property = $model->primary
×
269
            ?? throw new LogicException("Unable to update primary key, model `$model_class` doesn't define one.");
×
270

271
        $this->$property = $primary_key;
272
    }
273

274
    /**
275
     * Deletes the active record using its model.
276
     *
277
     * @throws LogicException in an attempt to delete a record from a model which primary key is empty.
278
     */
279
    public function delete(): void
280
    {
281
        $model = $this->get_model();
2✔
282
        $model_class = $model::class;
2✔
283
        $primary = $model->primary
2✔
284
            ?? throw new LogicException("Unable to delete record, model `$model_class` doesn't have a primary key");
×
285
        $key = $this->$primary
2✔
286
            ?? throw new LogicException("Unable to delete record, the primary key is not defined");
1✔
287

288
        $model->delete($key);
1✔
289
    }
290
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc