Skip to content

Commit e84defa

Browse files
committed
Seed sample vehicles for fixture members
1 parent b7ba967 commit e84defa

2 files changed

Lines changed: 495 additions & 0 deletions

File tree

‎lib/GaletteAuto/Fixtures.php‎

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
<?php
2+
3+
/**
4+
* This file is part of Galette (https://galette.eu).
5+
* SPDX-FileCopyrightText: Copyright © 2003-2026 The Galette Team
6+
* SPDX-License-Identifier: GPL-3.0-or-later
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace GaletteAuto;
12+
13+
use Galette\Core\Plugins\FixturesContext;
14+
use Galette\Core\Plugins\FixturesProviderInterface;
15+
use Galette\Entity\Adherent;
16+
use GaletteAuto\Repository\Vehicles;
17+
use RuntimeException;
18+
19+
/**
20+
* Sample vehicles for fixture members, run by galette:seed-fixtures
21+
*
22+
* Reference values (bodies, colors, brands...) are looked up by name and
23+
* created if missing. Vehicles are found back by their registration; once
24+
* removed, reference values of fixtures nothing uses anymore are removed too,
25+
* even if they were there before.
26+
*
27+
* @author Johan Cwiklinski <johan@x-tnd.be>
28+
* @phpstan-type Vehicle array{
29+
* name: string,
30+
* registration: string,
31+
* brand: string,
32+
* model: string,
33+
* fuel: int,
34+
* color: string,
35+
* body: string,
36+
* finition: string,
37+
* state: string,
38+
* transmission: string,
39+
* years: int,
40+
* mileage: int,
41+
* seats: int,
42+
* horsepower: int,
43+
* member: int,
44+
* comment?: string,
45+
* previous_member?: int
46+
* }
47+
*/
48+
class Fixtures implements FixturesProviderInterface
49+
{
50+
/** @var array<class-string<AbstractObject>, list<string>> Reference values */
51+
private const array VALUES = [
52+
Body::class => ['Berline', 'Break', 'Citadine', 'SUV', 'Monospace', 'Utilitaire', 'Cabriolet'],
53+
Color::class => ['Blanc', 'Noir', 'Gris métallisé', 'Bleu nuit', 'Rouge', 'Vert anglais', 'Jaune'],
54+
Finition::class => ['Standard', 'Confort', 'Sport', 'Luxe'],
55+
State::class => ['Neuf', 'Très bon état', 'Bon état', 'À restaurer'],
56+
Transmission::class => ['Manuelle', 'Automatique'],
57+
];
58+
59+
/** @var array<string, list<string>> Models, by brand */
60+
private const array MODELS = [
61+
'Renault' => ['Clio', 'Mégane', 'Zoé', 'Kangoo', '4L'],
62+
'Peugeot' => ['208', '308', '3008', 'Partner'],
63+
'Citroën' => ['C3', 'Berlingo', '2CV'],
64+
'Volkswagen' => ['Golf', 'Polo', 'Combi'],
65+
'Toyota' => ['Yaris', 'Prius'],
66+
'Tesla' => ['Model 3'],
67+
];
68+
69+
/** @var list<Vehicle> Vehicles; years since first registration, member by position */
70+
private const array VEHICLES = [
71+
['name' => 'Titine', 'registration' => 'FX-001-AA', 'brand' => 'Renault', 'model' => 'Clio',
72+
'fuel' => Auto::FUEL_PETROL, 'color' => 'Rouge', 'body' => 'Citadine', 'finition' => 'Standard',
73+
'state' => 'Bon état', 'transmission' => 'Manuelle', 'years' => 9, 'mileage' => 142300, 'seats' => 5,
74+
'horsepower' => 5, 'member' => 0],
75+
['name' => 'La familiale', 'registration' => 'FX-002-AB', 'brand' => 'Peugeot', 'model' => '3008',
76+
'fuel' => Auto::FUEL_DIESEL, 'color' => 'Gris métallisé', 'body' => 'SUV', 'finition' => 'Confort',
77+
'state' => 'Très bon état', 'transmission' => 'Automatique', 'years' => 4, 'mileage' => 61200,
78+
'seats' => 5, 'horsepower' => 7, 'member' => 1],
79+
['name' => 'Deuche', 'registration' => 'FX-003-AC', 'brand' => 'Citroën', 'model' => '2CV',
80+
'fuel' => Auto::FUEL_PETROL, 'color' => 'Jaune', 'body' => 'Berline', 'finition' => 'Standard',
81+
'state' => 'À restaurer', 'transmission' => 'Manuelle', 'years' => 48, 'mileage' => 231000, 'seats' => 4,
82+
'horsepower' => 2, 'member' => 2, 'comment' => 'Capote à changer avant la sortie de printemps.',
83+
'previous_member' => 5],
84+
['name' => 'Électron', 'registration' => 'FX-004-AD', 'brand' => 'Renault', 'model' => 'Zoé',
85+
'fuel' => Auto::FUEL_ELECTRICITY, 'color' => 'Blanc', 'body' => 'Citadine', 'finition' => 'Confort',
86+
'state' => 'Très bon état', 'transmission' => 'Automatique', 'years' => 3, 'mileage' => 38900,
87+
'seats' => 5, 'horsepower' => 2, 'member' => 3],
88+
['name' => 'Le combi', 'registration' => 'FX-005-AE', 'brand' => 'Volkswagen', 'model' => 'Combi',
89+
'fuel' => Auto::FUEL_PETROL, 'color' => 'Vert anglais', 'body' => 'Monospace', 'finition' => 'Standard',
90+
'state' => 'Bon état', 'transmission' => 'Manuelle', 'years' => 39, 'mileage' => 312400, 'seats' => 7,
91+
'horsepower' => 9, 'member' => 4, 'comment' => 'Indispensable pour les sorties du club.'],
92+
['name' => 'Golfette', 'registration' => 'FX-006-AF', 'brand' => 'Volkswagen', 'model' => 'Golf',
93+
'fuel' => Auto::FUEL_DIESEL, 'color' => 'Bleu nuit', 'body' => 'Berline', 'finition' => 'Sport',
94+
'state' => 'Bon état', 'transmission' => 'Manuelle', 'years' => 7, 'mileage' => 118700, 'seats' => 5,
95+
'horsepower' => 6, 'member' => 6],
96+
['name' => 'Discrète', 'registration' => 'FX-007-AG', 'brand' => 'Toyota', 'model' => 'Prius',
97+
'fuel' => Auto::FUEL_HYBRID, 'color' => 'Gris métallisé', 'body' => 'Berline', 'finition' => 'Luxe',
98+
'state' => 'Très bon état', 'transmission' => 'Automatique', 'years' => 5, 'mileage' => 87400,
99+
'seats' => 5, 'horsepower' => 5, 'member' => 7],
100+
['name' => 'Fourgonnette', 'registration' => 'FX-008-AH', 'brand' => 'Renault', 'model' => 'Kangoo',
101+
'fuel' => Auto::FUEL_DIESEL, 'color' => 'Blanc', 'body' => 'Utilitaire', 'finition' => 'Standard',
102+
'state' => 'Bon état', 'transmission' => 'Manuelle', 'years' => 11, 'mileage' => 198200, 'seats' => 2,
103+
'horsepower' => 6, 'member' => 8, 'comment' => 'Prêtée pour les déménagements de matériel.'],
104+
['name' => 'Quatrelle', 'registration' => 'FX-009-AJ', 'brand' => 'Renault', 'model' => '4L',
105+
'fuel' => Auto::FUEL_PETROL, 'color' => 'Bleu nuit', 'body' => 'Citadine', 'finition' => 'Standard',
106+
'state' => 'Bon état', 'transmission' => 'Manuelle', 'years' => 41, 'mileage' => 154000, 'seats' => 4,
107+
'horsepower' => 4, 'member' => 10, 'previous_member' => 0],
108+
['name' => 'Fusée', 'registration' => 'FX-010-AK', 'brand' => 'Tesla', 'model' => 'Model 3',
109+
'fuel' => Auto::FUEL_ELECTRICITY, 'color' => 'Noir', 'body' => 'Berline', 'finition' => 'Luxe',
110+
'state' => 'Neuf', 'transmission' => 'Automatique', 'years' => 1, 'mileage' => 8200, 'seats' => 5,
111+
'horsepower' => 4, 'member' => 11],
112+
['name' => 'Petite citadine', 'registration' => 'FX-011-AL', 'brand' => 'Toyota', 'model' => 'Yaris',
113+
'fuel' => Auto::FUEL_HYBRID, 'color' => 'Rouge', 'body' => 'Citadine', 'finition' => 'Confort',
114+
'state' => 'Très bon état', 'transmission' => 'Automatique', 'years' => 2, 'mileage' => 21500,
115+
'seats' => 5, 'horsepower' => 4, 'member' => 12],
116+
['name' => 'Le break', 'registration' => 'FX-012-AM', 'brand' => 'Peugeot', 'model' => '308',
117+
'fuel' => Auto::FUEL_DIESEL, 'color' => 'Noir', 'body' => 'Break', 'finition' => 'Confort',
118+
'state' => 'Bon état', 'transmission' => 'Manuelle', 'years' => 8, 'mileage' => 167300, 'seats' => 5,
119+
'horsepower' => 6, 'member' => 13],
120+
['name' => 'Ludospace', 'registration' => 'FX-013-AN', 'brand' => 'Citroën', 'model' => 'Berlingo',
121+
'fuel' => Auto::FUEL_DIESEL, 'color' => 'Gris métallisé', 'body' => 'Monospace',
122+
'finition' => 'Standard', 'state' => 'Bon état', 'transmission' => 'Manuelle', 'years' => 6,
123+
'mileage' => 104800, 'seats' => 5, 'horsepower' => 6, 'member' => 15],
124+
['name' => 'Polochon', 'registration' => 'FX-014-AP', 'brand' => 'Volkswagen', 'model' => 'Polo',
125+
'fuel' => Auto::FUEL_PETROL, 'color' => 'Blanc', 'body' => 'Citadine', 'finition' => 'Sport',
126+
'state' => 'Très bon état', 'transmission' => 'Manuelle', 'years' => 3, 'mileage' => 29600, 'seats' => 5,
127+
'horsepower' => 5, 'member' => 16],
128+
['name' => 'Cabriole', 'registration' => 'FX-015-AQ', 'brand' => 'Peugeot', 'model' => '208',
129+
'fuel' => Auto::FUEL_PETROL, 'color' => 'Jaune', 'body' => 'Cabriolet', 'finition' => 'Sport',
130+
'state' => 'Bon état', 'transmission' => 'Manuelle', 'years' => 6, 'mileage' => 73100, 'seats' => 4,
131+
'horsepower' => 5, 'member' => 18],
132+
];
133+
134+
/** @var array<class-string<AbstractObject>, array<string, int>> Reference values identifiers */
135+
private array $values = [];
136+
/** @var array<string, int> Models identifiers, by "brand/model" */
137+
private array $models = [];
138+
139+
/**
140+
* Create reference values and vehicles of fixture members
141+
*
142+
* @param FixturesContext $context Fixtures context
143+
*/
144+
public function seedFixtures(FixturesContext $context): string
145+
{
146+
if ($context->members === []) {
147+
return 'No member to give vehicles to';
148+
}
149+
150+
$zdb = $context->zdb;
151+
foreach (self::VALUES as $class => $values) {
152+
foreach ($values as $value) {
153+
$this->values[$class][$value] = $this->getValueId($context, $class, $value);
154+
}
155+
}
156+
foreach (self::MODELS as $brand => $models) {
157+
$brand_id = $this->getValueId($context, Brand::class, $brand);
158+
foreach ($models as $model) {
159+
$this->models[$brand . '/' . $model] = $this->getModelId($context, $brand_id, $model);
160+
}
161+
}
162+
163+
$access = new VehicleAccess($zdb, $context->login, $context->preferences);
164+
$preferences = new AutoPreferences($context->preferences);
165+
$vehicles = new Vehicles($context->plugins, $zdb, $context->login, $context->history);
166+
$today = new \DateTimeImmutable('today');
167+
168+
foreach (self::VEHICLES as $position => $data) {
169+
$registration = $today->modify(sprintf('-%d years', $data['years']))
170+
->modify(sprintf('-%d days', ($position * 47) % 300));
171+
$vehicle = new Auto($context->plugins, $zdb);
172+
$post = [
173+
'name' => $data['name'],
174+
'registration' => $data['registration'],
175+
'first_registration_date' => $registration->format('Y-m-d'),
176+
'first_circulation_date' => $registration->format('Y-m-d'),
177+
'mileage' => (string)$data['mileage'],
178+
'seats' => (string)$data['seats'],
179+
'horsepower' => (string)$data['horsepower'],
180+
'fuel' => (string)$data['fuel'],
181+
'comment' => $data['comment'] ?? '',
182+
'model' => (string)$this->models[$data['brand'] . '/' . $data['model']],
183+
'color' => (string)$this->values[Color::class][$data['color']],
184+
'body' => (string)$this->values[Body::class][$data['body']],
185+
'finition' => (string)$this->values[Finition::class][$data['finition']],
186+
'state' => (string)$this->values[State::class][$data['state']],
187+
'transmission' => (string)$this->values[Transmission::class][$data['transmission']],
188+
'owner_id' => (string)$context->getMemberId($data['member']),
189+
];
190+
if (!$vehicle->check($post, $access, $preferences)) {
191+
throw new RuntimeException(sprintf(
192+
'Invalid fixture vehicle %s: %s',
193+
$data['name'],
194+
implode(', ', $vehicle->getErrors())
195+
));
196+
}
197+
$vehicles->store($vehicle);
198+
$this->backdate($context, $vehicle, $data, $position);
199+
}
200+
201+
return sprintf('Created %d vehicles', count(self::VEHICLES));
202+
}
203+
204+
/**
205+
* Vehicles are added today: pretend they have been added a while ago,
206+
* some of them by a former owner
207+
*
208+
* @param FixturesContext $context Fixtures context
209+
* @param Auto $vehicle Stored vehicle
210+
* @param Vehicle $data Vehicle data
211+
* @param int $position Vehicle position
212+
*/
213+
private function backdate(FixturesContext $context, Auto $vehicle, array $data, int $position): void
214+
{
215+
$zdb = $context->zdb;
216+
$added = (new \DateTimeImmutable('today'))->modify(sprintf('-%d days', 30 + ($position * 61) % 900));
217+
218+
$update = $zdb->update(AUTO_PREFIX . Auto::TABLE)
219+
->set(['car_creation_date' => $added->format('Y-m-d')])
220+
->where([Auto::PK => $vehicle->getId()]);
221+
$zdb->execute($update);
222+
223+
$update = $zdb->update(AUTO_PREFIX . History::TABLE)
224+
->set(['history_date' => $added->format('Y-m-d H:i:s')])
225+
->where([Auto::PK => $vehicle->getId()]);
226+
$zdb->execute($update);
227+
228+
$previous = isset($data['previous_member']) ? $context->getMemberId($data['previous_member']) : null;
229+
if ($previous !== null && $previous !== $context->getMemberId($data['member'])) {
230+
$insert = $zdb->insert(AUTO_PREFIX . History::TABLE)->values([
231+
Auto::PK => $vehicle->getId(),
232+
Adherent::PK => $previous,
233+
'history_date' => $added->modify('-3 years')->format('Y-m-d H:i:s'),
234+
'car_registration' => $data['registration'],
235+
Color::PK => $this->values[Color::class][$data['color']],
236+
State::PK => $this->values[State::class]['Très bon état'],
237+
]);
238+
$zdb->execute($insert);
239+
}
240+
}
241+
242+
/**
243+
* Get reference value identifier, create it if missing
244+
*
245+
* @param FixturesContext $context Fixtures context
246+
* @param class-string<AbstractObject> $class Value class
247+
* @param string $value Value
248+
*/
249+
private function getValueId(FixturesContext $context, string $class, string $value): int
250+
{
251+
$select = $context->zdb->select(AUTO_PREFIX . $class::TABLE)
252+
->columns([$class::PK])
253+
->where([$class::FIELD => $value])
254+
->limit(1);
255+
$row = $context->zdb->execute($select)->current();
256+
if ($row) {
257+
return (int)$row->{$class::PK};
258+
}
259+
260+
$object = new $class($context->zdb);
261+
$object->setValue($value);
262+
$object->store(true);
263+
return (int)$object->getId();
264+
}
265+
266+
/**
267+
* Get model identifier, create it if missing
268+
*
269+
* @param FixturesContext $context Fixtures context
270+
* @param int $brand_id Brand identifier
271+
* @param string $name Model name
272+
*/
273+
private function getModelId(FixturesContext $context, int $brand_id, string $name): int
274+
{
275+
$select = $context->zdb->select(AUTO_PREFIX . Model::TABLE)
276+
->columns([Model::PK])
277+
->where([Model::FIELD => $name, Brand::PK => $brand_id])
278+
->limit(1);
279+
$row = $context->zdb->execute($select)->current();
280+
if ($row) {
281+
return (int)$row->{Model::PK};
282+
}
283+
284+
$model = new Model($context->zdb);
285+
if (!$model->check(['brand' => $brand_id, 'model' => $name])) {
286+
throw new RuntimeException(sprintf('Invalid fixture model %s', $name));
287+
}
288+
$model->store(true);
289+
return (int)$model->getId();
290+
}
291+
292+
/**
293+
* Remove fixture vehicles, then reference values nothing uses anymore
294+
*
295+
* @param FixturesContext $context Fixtures context
296+
*/
297+
public function cleanFixtures(FixturesContext $context): void
298+
{
299+
$zdb = $context->zdb;
300+
301+
$select = $zdb->select(AUTO_PREFIX . Auto::TABLE)->columns([Auto::PK]);
302+
$select->where->in('car_registration', array_column(self::VEHICLES, 'registration'));
303+
$ids = [];
304+
foreach ($zdb->execute($select) as $row) {
305+
$ids[] = (int)$row->{Auto::PK};
306+
}
307+
if ($ids !== []) {
308+
(new Vehicles($context->plugins, $zdb, $context->login, $context->history))->remove($ids);
309+
}
310+
311+
foreach (self::MODELS as $brand => $models) {
312+
$select = $zdb->select(AUTO_PREFIX . Brand::TABLE)->columns([Brand::PK])->where([Brand::FIELD => $brand]);
313+
foreach ($zdb->execute($select) as $row) {
314+
$this->removeUnused(
315+
$context,
316+
Model::TABLE,
317+
Model::PK,
318+
[Model::FIELD => $models, Brand::PK => (int)$row->{Brand::PK}],
319+
[Auto::TABLE]
320+
);
321+
}
322+
}
323+
$this->removeUnused(
324+
$context,
325+
Brand::TABLE,
326+
Brand::PK,
327+
[Brand::FIELD => array_keys(self::MODELS)],
328+
[Model::TABLE]
329+
);
330+
331+
foreach (self::VALUES as $class => $values) {
332+
//history keeps colors and states too
333+
$referencers = in_array($class, [Color::class, State::class], true)
334+
? [Auto::TABLE, History::TABLE]
335+
: [Auto::TABLE];
336+
$this->removeUnused($context, $class::TABLE, $class::PK, [$class::FIELD => $values], $referencers);
337+
}
338+
}
339+
340+
/**
341+
* Remove rows nothing references anymore
342+
*
343+
* @param FixturesContext $context Fixtures context
344+
* @param string $table Table name, without prefixes
345+
* @param string $pk Primary key, also the referencing column
346+
* @param array<string, list<string>|int> $where Rows to remove; lists are IN conditions
347+
* @param list<string> $referencers Referencing tables names, without prefixes
348+
*/
349+
private function removeUnused(
350+
FixturesContext $context,
351+
string $table,
352+
string $pk,
353+
array $where,
354+
array $referencers
355+
): void {
356+
$zdb = $context->zdb;
357+
$used = [];
358+
foreach ($referencers as $referencer) {
359+
$select = $zdb->select(AUTO_PREFIX . $referencer)->columns([$pk])->quantifier('DISTINCT');
360+
foreach ($zdb->execute($select) as $row) {
361+
$used[] = (int)$row->{$pk};
362+
}
363+
}
364+
365+
$delete = $zdb->delete(AUTO_PREFIX . $table);
366+
foreach ($where as $column => $value) {
367+
if (is_array($value)) {
368+
$delete->where->in($column, $value);
369+
} else {
370+
$delete->where->equalTo($column, $value);
371+
}
372+
}
373+
if ($used !== []) {
374+
$delete->where->notIn($pk, $used);
375+
}
376+
$zdb->execute($delete);
377+
}
378+
}

0 commit comments

Comments
 (0)