Skip to content

Commit

Permalink
chore(seeder): add seeders
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
tomudding committed Nov 13, 2024
1 parent e997d06 commit 3666363
Show file tree
Hide file tree
Showing 20 changed files with 926 additions and 24 deletions.
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,16 @@ rundev: builddev
@make replenish
@docker compose exec web rm -rf data/cache/module-config-cache.application.config.cache.php

migrate: replenish
@docker compose exec -it web ./orm migrations:migrate

migration-list: replenish
@docker compose exec -T web ./orm migrations:list

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

migration-migrate: replenish
@docker compose exec -it web ./orm migrations:migrate

migration-up: replenish migration-list
@read -p "Enter the migration version to execute (e.g., Application\\Migrations\\Version20241020212355 -- note escaping the backslashes is required): " version; \
docker compose exec -it web ./orm migrations:execute --up $$version
Expand All @@ -60,6 +60,9 @@ migration-down: replenish migration-list
@read -p "Enter the migration version to down (e.g., Application\\Migrations\\Version20241020212355 -- note escaping the backslashes is required): " version; \
docker compose exec -it web ./orm migrations:execute --down $$version

seed: replenish
@docker compose exec -T web ./web application:fixtures:load

exec:
docker compose exec -it web $(cmd)

Expand Down
7 changes: 7 additions & 0 deletions config/autoload/doctrine.local.development.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

declare(strict_types=1);

use Decision\Extensions\Doctrine\MeetingTypesType;
use Doctrine\DBAL\Driver\PDO\MySQL\Driver;

return [
Expand All @@ -41,6 +42,9 @@ return [
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
] : [],
],
'doctrineTypeMappings' => [
MeetingTypesType::NAME => MeetingTypesType::NAME,
],
],
],
// Configuration details for the ORM.
Expand Down Expand Up @@ -73,6 +77,9 @@ return [
],
// Second level cache configuration (see doc to learn about configuration)
'second_level_cache' => [],
'types' => [
MeetingTypesType::NAME => MeetingTypesType::class,
],
],
],
'migrations_configuration' => [
Expand Down
6 changes: 6 additions & 0 deletions module/Application/config/module.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Application;

use Application\Command\LoadFixtures;
use Application\Controller\Factory\IndexControllerFactory;
use Application\Controller\IndexController;
use Application\View\Helper\BootstrapElementError;
Expand Down Expand Up @@ -148,6 +149,11 @@
'message_separator_string' => '</li><li>',
],
],
'laminas-cli' => [
'commands' => [
'application:fixtures:load' => LoadFixtures::class,
],
],
'doctrine' => [
'driver' => [
__NAMESPACE__ . '_driver' => [
Expand Down
27 changes: 27 additions & 0 deletions module/Application/src/Command/Factory/LoadFixturesFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Application\Command\Factory;

use Application\Command\LoadFixtures;
use Doctrine\ORM\EntityManager;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Psr\Container\ContainerInterface;

class LoadFixturesFactory implements FactoryInterface
{
/**
* @param string $requestedName
*/
public function __invoke(
ContainerInterface $container,
$requestedName,
?array $options = null,
): LoadFixtures {
/** @var EntityManager $entityManager */
$entityManager = $container->get('doctrine.entitymanager.orm_default');

return new LoadFixtures($entityManager);
}
}
68 changes: 68 additions & 0 deletions module/Application/src/Command/LoadFixtures.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Application\Command;

use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Loader;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;

#[AsCommand(
name: 'application:fixtures:load',
description: 'Seed the database with data fixtures.',
)]
class LoadFixtures extends Command
{
private const array FIXTURES = [
// './module/Activity/test/Seeder',
// './module/Company/test/Seeder',
'./module/Decision/test/Seeder',
// './module/Education/test/Seeder',
// './module/Frontpage/test/Seeder',
// './module/Photo/test/Seeder',
'./module/User/test/Seeder',
];

public function __construct(private readonly EntityManager $entityManager)
{
parent::__construct();
}

protected function execute(
InputInterface $input,
OutputInterface $output,
): int {
$loader = new Loader();
$purger = new ORMPurger();
$purger->setPurgeMode(ORMPurger::PURGE_MODE_TRUNCATE);
$executor = new ORMExecutor($this->entityManager, $purger);

foreach ($this::FIXTURES as $fixture) {
$loader->loadFromDirectory($fixture);
}

$output->writeln('<info>Loading fixtures into the database...</info>');

$connection = $this->entityManager->getConnection();
try {
// Temporarily disable FK constraint checks. This is necessary because large parts of our database do not have

Check warning on line 55 in module/Application/src/Command/LoadFixtures.php

View workflow job for this annotation

GitHub Actions / php-codesniffer / PHP_CodeSniffer (8.3)

Line exceeds 120 characters; contains 122 characters
// explicit CASCADEs set to prevent data loss when syncing with ReportDB (GEWISDB).
// The try-catch is necessary to hide some error messages (because the executeStatement).
$connection->executeStatement('SET FOREIGN_KEY_CHECKS = 0');
$executor->execute($loader->getFixtures());
$connection->executeStatement('SET FOREIGN_KEY_CHECKS = 1');
} catch (Throwable) {
}

$output->writeln('<info>Loaded fixtures!</info>');

return Command::SUCCESS;
}
}
72 changes: 72 additions & 0 deletions module/Application/src/Extensions/Doctrine/BackedEnumType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace Application\Extensions\Doctrine;

use BackedEnum;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;

use function call_user_func;

/**
* Custom mapping type for Doctrine DBAL to directly support enums in a database without having to use a native type.
*
* It is necessary to use this custom mapping type due to an apparent bug in the value conversion layer in DBAL when
* using trying to construct our (Sub)Decisions in specific scenarios (e.g. seeding the database).
*
* Due to the `final` marking of the constructor we cannot initialise {@link BackedEnumType::$enumClass} and
* {@link BackedEnumType::$name}. As such, we need to override these when creating specific mapping types.
*
* @template T of BackedEnum
*/
abstract class BackedEnumType extends Type
{
/**
* @var class-string<T> $enumClass
* @required
*/
public string $enumClass;

/**
* @required
*/
public const string NAME = '';

/**
* {@inheritDoc}
*
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification
*/
public function getSQLDeclaration(
array $column,
AbstractPlatform $platform,
): string {
return $platform->getStringTypeDeclarationSQL($column);
}

/**
* @return T|null
*/
public function convertToPHPValue(
mixed $value,
AbstractPlatform $platform,
) {
if (empty($value)) {
return null;
}

return call_user_func([$this->enumClass, 'from'], $value);
}

/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingAnyTypeHint
*/
public function convertToDatabaseValue(
mixed $value,
AbstractPlatform $platform,
) {
return $value instanceof $this->enumClass ? $value->value : $value;
}
}
3 changes: 3 additions & 0 deletions module/Application/src/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Application;

use Application\Command\Factory\LoadFixturesFactory as LoadFixturesCommandFactory;
use Application\Command\LoadFixtures as LoadFixturesCommand;
use Application\Extensions\CommonMark\CompanyImage\CompanyImageExtension;
use Application\Extensions\CommonMark\NoImage\NoImageExtension;
use Application\Extensions\CommonMark\VideoIframe\VideoIframeExtension;
Expand Down Expand Up @@ -266,6 +268,7 @@ public function generateSignature(

return new UrlBuilder($config['glide']['base_url'], $signature);
},
LoadFixturesCommand::class => LoadFixturesCommandFactory::class,
],
];
}
Expand Down
23 changes: 23 additions & 0 deletions module/Decision/src/Extensions/Doctrine/MeetingTypesType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Decision\Extensions\Doctrine;

use Application\Extensions\Doctrine\BackedEnumType;
use Decision\Model\Enums\MeetingTypes;

/**
* @extends BackedEnumType<MeetingTypes>
*/
class MeetingTypesType extends BackedEnumType
{
public string $enumClass = MeetingTypes::class;

public const string NAME = 'meeting_types';

public function getName(): string
{
return self::NAME;
}
}
4 changes: 2 additions & 2 deletions module/Decision/src/Model/AssociationYear.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class AssociationYear
/**
* A GEWIS association year starts 01-07.
*/
public const ASSOCIATION_YEAR_START_MONTH = 7;
public const ASSOCIATION_YEAR_START_DAY = 1;
public const int ASSOCIATION_YEAR_START_MONTH = 7;
public const int ASSOCIATION_YEAR_START_DAY = 1;

/** @var int the first calendar year of the association year */
protected int $firstYear;
Expand Down
15 changes: 10 additions & 5 deletions module/Decision/src/Model/Decision.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

namespace Decision\Model;

use Decision\Extensions\Doctrine\MeetingTypesType;
use Decision\Model\Enums\MeetingTypes;
use Decision\Model\SubDecision\Annulment;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
Expand Down Expand Up @@ -47,11 +49,9 @@ class Decision
* NOTE: This is a hack to make the meeting a primary key here.
*/
#[Id]
#[Column(
type: 'string',
enumType: MeetingTypes::class,
)]
#[Column(type: MeetingTypesType::NAME)]
protected MeetingTypes $meeting_type;

/**
* Meeting number.
*
Expand Down Expand Up @@ -103,7 +103,12 @@ enumType: MeetingTypes::class,
targetEntity: Annulment::class,
mappedBy: 'target',
)]
protected Annulment $annulledBy;
protected ?Annulment $annulledBy = null;

public function __construct()
{
$this->subdecisions = new ArrayCollection();
}

/**
* Set the meeting.
Expand Down
6 changes: 2 additions & 4 deletions module/Decision/src/Model/Meeting.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Decision\Model;

use DateTime;
use Decision\Extensions\Doctrine\MeetingTypesType;
use Decision\Model\Enums\MeetingTypes;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
Expand All @@ -25,10 +26,7 @@ class Meeting
* Meeting type.
*/
#[Id]
#[Column(
type: 'string',
enumType: MeetingTypes::class,
)]
#[Column(type: MeetingTypesType::NAME)]
protected MeetingTypes $type;

/**
Expand Down
6 changes: 2 additions & 4 deletions module/Decision/src/Model/MeetingMinutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Decision\Model;

use Application\Model\Traits\TimestampableTrait;
use Decision\Extensions\Doctrine\MeetingTypesType;
use Decision\Model\Enums\MeetingTypes;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
Expand All @@ -27,10 +28,7 @@ class MeetingMinutes implements ResourceInterface
* Meeting type.
*/
#[Id]
#[Column(
type: 'string',
enumType: MeetingTypes::class,
)]
#[Column(type: MeetingTypesType::NAME)]
protected MeetingTypes $meeting_type;

/**
Expand Down
6 changes: 2 additions & 4 deletions module/Decision/src/Model/SubDecision.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Decision\Model;

use Decision\Extensions\Doctrine\MeetingTypesType;
use Decision\Model\Enums\MeetingTypes;
use Decision\Model\SubDecision\Abrogation;
use Decision\Model\SubDecision\Annulment;
Expand Down Expand Up @@ -99,10 +100,7 @@ abstract class SubDecision
* NOTE: This is a hack to make the decision a primary key here.
*/
#[Id]
#[Column(
type: 'string',
enumType: MeetingTypes::class,
)]
#[Column(type: MeetingTypesType::NAME)]
protected MeetingTypes $meeting_type;

/**
Expand Down
Loading

0 comments on commit 3666363

Please sign in to comment.