Skip to content

Commit 3666363

Browse files
committed
chore(seeder): add seeders
The data fixtures can be loaded into the database using the `application:fixtures:load` command. All existing records are `TRUNCATE`d from the database to ensure a clean start. --- Conclusion: too many issues with hydration of the enums that are part of our composite keys. I have tried adding a custom mapping type to resolve this issue. Unfortunately, that breaks it outside of seeding the database (so the whole website). --- This also fixes some inconsistencies in the (sub)decision model with GEWISDB, somehow the possibility for these to be `null` got lost somewhere (and fixes for initialisation of `Collection`s).
1 parent e997d06 commit 3666363

File tree

20 files changed

+926
-24
lines changed

20 files changed

+926
-24
lines changed

Makefile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,16 @@ rundev: builddev
4242
@make replenish
4343
@docker compose exec web rm -rf data/cache/module-config-cache.application.config.cache.php
4444

45+
migrate: replenish
46+
@docker compose exec -it web ./orm migrations:migrate
47+
4548
migration-list: replenish
4649
@docker compose exec -T web ./orm migrations:list
4750

4851
migration-diff: replenish
4952
@docker compose exec -T web ./orm migrations:diff
5053
@docker cp "$(shell docker compose ps -q web)":/code/module/Application/migrations ./module/Application/migrations
5154

52-
migration-migrate: replenish
53-
@docker compose exec -it web ./orm migrations:migrate
54-
5555
migration-up: replenish migration-list
5656
@read -p "Enter the migration version to execute (e.g., Application\\Migrations\\Version20241020212355 -- note escaping the backslashes is required): " version; \
5757
docker compose exec -it web ./orm migrations:execute --up $$version
@@ -60,6 +60,9 @@ migration-down: replenish migration-list
6060
@read -p "Enter the migration version to down (e.g., Application\\Migrations\\Version20241020212355 -- note escaping the backslashes is required): " version; \
6161
docker compose exec -it web ./orm migrations:execute --down $$version
6262

63+
seed: replenish
64+
@docker compose exec -T web ./web application:fixtures:load
65+
6366
exec:
6467
docker compose exec -it web $(cmd)
6568

config/autoload/doctrine.local.development.php.dist

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
declare(strict_types=1);
2222

23+
use Decision\Extensions\Doctrine\MeetingTypesType;
2324
use Doctrine\DBAL\Driver\PDO\MySQL\Driver;
2425

2526
return [
@@ -41,6 +42,9 @@ return [
4142
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
4243
] : [],
4344
],
45+
'doctrineTypeMappings' => [
46+
MeetingTypesType::NAME => MeetingTypesType::NAME,
47+
],
4448
],
4549
],
4650
// Configuration details for the ORM.
@@ -73,6 +77,9 @@ return [
7377
],
7478
// Second level cache configuration (see doc to learn about configuration)
7579
'second_level_cache' => [],
80+
'types' => [
81+
MeetingTypesType::NAME => MeetingTypesType::class,
82+
],
7683
],
7784
],
7885
'migrations_configuration' => [

module/Application/config/module.config.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Application;
66

7+
use Application\Command\LoadFixtures;
78
use Application\Controller\Factory\IndexControllerFactory;
89
use Application\Controller\IndexController;
910
use Application\View\Helper\BootstrapElementError;
@@ -148,6 +149,11 @@
148149
'message_separator_string' => '</li><li>',
149150
],
150151
],
152+
'laminas-cli' => [
153+
'commands' => [
154+
'application:fixtures:load' => LoadFixtures::class,
155+
],
156+
],
151157
'doctrine' => [
152158
'driver' => [
153159
__NAMESPACE__ . '_driver' => [
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Application\Command\Factory;
6+
7+
use Application\Command\LoadFixtures;
8+
use Doctrine\ORM\EntityManager;
9+
use Laminas\ServiceManager\Factory\FactoryInterface;
10+
use Psr\Container\ContainerInterface;
11+
12+
class LoadFixturesFactory implements FactoryInterface
13+
{
14+
/**
15+
* @param string $requestedName
16+
*/
17+
public function __invoke(
18+
ContainerInterface $container,
19+
$requestedName,
20+
?array $options = null,
21+
): LoadFixtures {
22+
/** @var EntityManager $entityManager */
23+
$entityManager = $container->get('doctrine.entitymanager.orm_default');
24+
25+
return new LoadFixtures($entityManager);
26+
}
27+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Application\Command;
6+
7+
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
8+
use Doctrine\Common\DataFixtures\Loader;
9+
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
10+
use Doctrine\ORM\EntityManager;
11+
use Symfony\Component\Console\Attribute\AsCommand;
12+
use Symfony\Component\Console\Command\Command;
13+
use Symfony\Component\Console\Input\InputInterface;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
use Throwable;
16+
17+
#[AsCommand(
18+
name: 'application:fixtures:load',
19+
description: 'Seed the database with data fixtures.',
20+
)]
21+
class LoadFixtures extends Command
22+
{
23+
private const array FIXTURES = [
24+
// './module/Activity/test/Seeder',
25+
// './module/Company/test/Seeder',
26+
'./module/Decision/test/Seeder',
27+
// './module/Education/test/Seeder',
28+
// './module/Frontpage/test/Seeder',
29+
// './module/Photo/test/Seeder',
30+
'./module/User/test/Seeder',
31+
];
32+
33+
public function __construct(private readonly EntityManager $entityManager)
34+
{
35+
parent::__construct();
36+
}
37+
38+
protected function execute(
39+
InputInterface $input,
40+
OutputInterface $output,
41+
): int {
42+
$loader = new Loader();
43+
$purger = new ORMPurger();
44+
$purger->setPurgeMode(ORMPurger::PURGE_MODE_TRUNCATE);
45+
$executor = new ORMExecutor($this->entityManager, $purger);
46+
47+
foreach ($this::FIXTURES as $fixture) {
48+
$loader->loadFromDirectory($fixture);
49+
}
50+
51+
$output->writeln('<info>Loading fixtures into the database...</info>');
52+
53+
$connection = $this->entityManager->getConnection();
54+
try {
55+
// Temporarily disable FK constraint checks. This is necessary because large parts of our database do not have
56+
// explicit CASCADEs set to prevent data loss when syncing with ReportDB (GEWISDB).
57+
// The try-catch is necessary to hide some error messages (because the executeStatement).
58+
$connection->executeStatement('SET FOREIGN_KEY_CHECKS = 0');
59+
$executor->execute($loader->getFixtures());
60+
$connection->executeStatement('SET FOREIGN_KEY_CHECKS = 1');
61+
} catch (Throwable) {
62+
}
63+
64+
$output->writeln('<info>Loaded fixtures!</info>');
65+
66+
return Command::SUCCESS;
67+
}
68+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Application\Extensions\Doctrine;
6+
7+
use BackedEnum;
8+
use Doctrine\DBAL\Platforms\AbstractPlatform;
9+
use Doctrine\DBAL\Types\Type;
10+
11+
use function call_user_func;
12+
13+
/**
14+
* Custom mapping type for Doctrine DBAL to directly support enums in a database without having to use a native type.
15+
*
16+
* It is necessary to use this custom mapping type due to an apparent bug in the value conversion layer in DBAL when
17+
* using trying to construct our (Sub)Decisions in specific scenarios (e.g. seeding the database).
18+
*
19+
* Due to the `final` marking of the constructor we cannot initialise {@link BackedEnumType::$enumClass} and
20+
* {@link BackedEnumType::$name}. As such, we need to override these when creating specific mapping types.
21+
*
22+
* @template T of BackedEnum
23+
*/
24+
abstract class BackedEnumType extends Type
25+
{
26+
/**
27+
* @var class-string<T> $enumClass
28+
* @required
29+
*/
30+
public string $enumClass;
31+
32+
/**
33+
* @required
34+
*/
35+
public const string NAME = '';
36+
37+
/**
38+
* {@inheritDoc}
39+
*
40+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification
41+
*/
42+
public function getSQLDeclaration(
43+
array $column,
44+
AbstractPlatform $platform,
45+
): string {
46+
return $platform->getStringTypeDeclarationSQL($column);
47+
}
48+
49+
/**
50+
* @return T|null
51+
*/
52+
public function convertToPHPValue(
53+
mixed $value,
54+
AbstractPlatform $platform,
55+
) {
56+
if (empty($value)) {
57+
return null;
58+
}
59+
60+
return call_user_func([$this->enumClass, 'from'], $value);
61+
}
62+
63+
/**
64+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingAnyTypeHint
65+
*/
66+
public function convertToDatabaseValue(
67+
mixed $value,
68+
AbstractPlatform $platform,
69+
) {
70+
return $value instanceof $this->enumClass ? $value->value : $value;
71+
}
72+
}

module/Application/src/Module.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Application;
66

7+
use Application\Command\Factory\LoadFixturesFactory as LoadFixturesCommandFactory;
8+
use Application\Command\LoadFixtures as LoadFixturesCommand;
79
use Application\Extensions\CommonMark\CompanyImage\CompanyImageExtension;
810
use Application\Extensions\CommonMark\NoImage\NoImageExtension;
911
use Application\Extensions\CommonMark\VideoIframe\VideoIframeExtension;
@@ -266,6 +268,7 @@ public function generateSignature(
266268

267269
return new UrlBuilder($config['glide']['base_url'], $signature);
268270
},
271+
LoadFixturesCommand::class => LoadFixturesCommandFactory::class,
269272
],
270273
];
271274
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Decision\Extensions\Doctrine;
6+
7+
use Application\Extensions\Doctrine\BackedEnumType;
8+
use Decision\Model\Enums\MeetingTypes;
9+
10+
/**
11+
* @extends BackedEnumType<MeetingTypes>
12+
*/
13+
class MeetingTypesType extends BackedEnumType
14+
{
15+
public string $enumClass = MeetingTypes::class;
16+
17+
public const string NAME = 'meeting_types';
18+
19+
public function getName(): string
20+
{
21+
return self::NAME;
22+
}
23+
}

module/Decision/src/Model/AssociationYear.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ class AssociationYear
1515
/**
1616
* A GEWIS association year starts 01-07.
1717
*/
18-
public const ASSOCIATION_YEAR_START_MONTH = 7;
19-
public const ASSOCIATION_YEAR_START_DAY = 1;
18+
public const int ASSOCIATION_YEAR_START_MONTH = 7;
19+
public const int ASSOCIATION_YEAR_START_DAY = 1;
2020

2121
/** @var int the first calendar year of the association year */
2222
protected int $firstYear;

module/Decision/src/Model/Decision.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
namespace Decision\Model;
66

7+
use Decision\Extensions\Doctrine\MeetingTypesType;
78
use Decision\Model\Enums\MeetingTypes;
89
use Decision\Model\SubDecision\Annulment;
10+
use Doctrine\Common\Collections\ArrayCollection;
911
use Doctrine\Common\Collections\Collection;
1012
use Doctrine\ORM\Mapping\Column;
1113
use Doctrine\ORM\Mapping\Entity;
@@ -47,11 +49,9 @@ class Decision
4749
* NOTE: This is a hack to make the meeting a primary key here.
4850
*/
4951
#[Id]
50-
#[Column(
51-
type: 'string',
52-
enumType: MeetingTypes::class,
53-
)]
52+
#[Column(type: MeetingTypesType::NAME)]
5453
protected MeetingTypes $meeting_type;
54+
5555
/**
5656
* Meeting number.
5757
*
@@ -103,7 +103,12 @@ enumType: MeetingTypes::class,
103103
targetEntity: Annulment::class,
104104
mappedBy: 'target',
105105
)]
106-
protected Annulment $annulledBy;
106+
protected ?Annulment $annulledBy = null;
107+
108+
public function __construct()
109+
{
110+
$this->subdecisions = new ArrayCollection();
111+
}
107112

108113
/**
109114
* Set the meeting.

0 commit comments

Comments
 (0)