diff --git a/Makefile b/Makefile
index 5693aa39fc..3bee454cd8 100644
--- a/Makefile
+++ b/Makefile
@@ -42,6 +42,9 @@ 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
@@ -49,9 +52,6 @@ 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
@@ -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)
diff --git a/config/autoload/doctrine.local.development.php.dist b/config/autoload/doctrine.local.development.php.dist
index dc6f9addf0..c0295ca50b 100644
--- a/config/autoload/doctrine.local.development.php.dist
+++ b/config/autoload/doctrine.local.development.php.dist
@@ -20,6 +20,7 @@
declare(strict_types=1);
+use Decision\Extensions\Doctrine\MeetingTypesType;
use Doctrine\DBAL\Driver\PDO\MySQL\Driver;
return [
@@ -41,6 +42,9 @@ return [
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
] : [],
],
+ 'doctrineTypeMappings' => [
+ MeetingTypesType::NAME => MeetingTypesType::NAME,
+ ],
],
],
// Configuration details for the ORM.
@@ -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' => [
diff --git a/module/Application/config/module.config.php b/module/Application/config/module.config.php
index 1ee5a56cac..d3128e6a03 100644
--- a/module/Application/config/module.config.php
+++ b/module/Application/config/module.config.php
@@ -4,6 +4,7 @@
namespace Application;
+use Application\Command\LoadFixtures;
use Application\Controller\Factory\IndexControllerFactory;
use Application\Controller\IndexController;
use Application\View\Helper\BootstrapElementError;
@@ -148,6 +149,11 @@
'message_separator_string' => '
',
],
],
+ 'laminas-cli' => [
+ 'commands' => [
+ 'application:fixtures:load' => LoadFixtures::class,
+ ],
+ ],
'doctrine' => [
'driver' => [
__NAMESPACE__ . '_driver' => [
diff --git a/module/Application/src/Command/Factory/LoadFixturesFactory.php b/module/Application/src/Command/Factory/LoadFixturesFactory.php
new file mode 100644
index 0000000000..86002ca5e2
--- /dev/null
+++ b/module/Application/src/Command/Factory/LoadFixturesFactory.php
@@ -0,0 +1,27 @@
+get('doctrine.entitymanager.orm_default');
+
+ return new LoadFixtures($entityManager);
+ }
+}
diff --git a/module/Application/src/Command/LoadFixtures.php b/module/Application/src/Command/LoadFixtures.php
new file mode 100644
index 0000000000..50e937e57e
--- /dev/null
+++ b/module/Application/src/Command/LoadFixtures.php
@@ -0,0 +1,68 @@
+setPurgeMode(ORMPurger::PURGE_MODE_TRUNCATE);
+ $executor = new ORMExecutor($this->entityManager, $purger);
+
+ foreach ($this::FIXTURES as $fixture) {
+ $loader->loadFromDirectory($fixture);
+ }
+
+ $output->writeln('Loading fixtures into the database...');
+
+ $connection = $this->entityManager->getConnection();
+ try {
+ // Temporarily disable FK constraint checks. This is necessary because large parts of our database do not have
+ // 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('Loaded fixtures!');
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/module/Application/src/Extensions/Doctrine/BackedEnumType.php b/module/Application/src/Extensions/Doctrine/BackedEnumType.php
new file mode 100644
index 0000000000..ad98579961
--- /dev/null
+++ b/module/Application/src/Extensions/Doctrine/BackedEnumType.php
@@ -0,0 +1,72 @@
+ $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);
+ }
+
+ /**
+ * @param mixed $value
+ *
+ * @return T|null
+ */
+ public function convertToPHPValue(
+ $value,
+ AbstractPlatform $platform,
+ ) {
+ if (empty($value)) {
+ return null;
+ }
+
+ return call_user_func([$this->enumClass, 'from'], $value);
+ }
+
+ public function convertToDatabaseValue(
+ $value,
+ AbstractPlatform $platform,
+ ) {
+ return $value instanceof $this->enumClass ? $value->value : $value;
+ }
+}
diff --git a/module/Application/src/Module.php b/module/Application/src/Module.php
index d60dbf6fe9..7c1bf319f5 100644
--- a/module/Application/src/Module.php
+++ b/module/Application/src/Module.php
@@ -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;
@@ -266,6 +268,7 @@ public function generateSignature(
return new UrlBuilder($config['glide']['base_url'], $signature);
},
+ LoadFixturesCommand::class => LoadFixturesCommandFactory::class,
],
];
}
diff --git a/module/Decision/src/Extensions/Doctrine/MeetingTypesType.php b/module/Decision/src/Extensions/Doctrine/MeetingTypesType.php
new file mode 100644
index 0000000000..ae9b3f9243
--- /dev/null
+++ b/module/Decision/src/Extensions/Doctrine/MeetingTypesType.php
@@ -0,0 +1,23 @@
+
+ */
+class MeetingTypesType extends BackedEnumType
+{
+ public string $enumClass = MeetingTypes::class;
+
+ public const string NAME = 'meeting_types';
+
+ public function getName(): string
+ {
+ return self::NAME;
+ }
+}
diff --git a/module/Decision/src/Model/AssociationYear.php b/module/Decision/src/Model/AssociationYear.php
index b5541f970a..b6fa36ccf6 100644
--- a/module/Decision/src/Model/AssociationYear.php
+++ b/module/Decision/src/Model/AssociationYear.php
@@ -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;
diff --git a/module/Decision/src/Model/Decision.php b/module/Decision/src/Model/Decision.php
index 815b7b25e3..b54e7bda92 100644
--- a/module/Decision/src/Model/Decision.php
+++ b/module/Decision/src/Model/Decision.php
@@ -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;
@@ -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.
*
@@ -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.
diff --git a/module/Decision/src/Model/Meeting.php b/module/Decision/src/Model/Meeting.php
index 4908f9e167..d96cb69cbb 100644
--- a/module/Decision/src/Model/Meeting.php
+++ b/module/Decision/src/Model/Meeting.php
@@ -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;
@@ -25,10 +26,7 @@ class Meeting
* Meeting type.
*/
#[Id]
- #[Column(
- type: 'string',
- enumType: MeetingTypes::class,
- )]
+ #[Column(type: MeetingTypesType::NAME)]
protected MeetingTypes $type;
/**
diff --git a/module/Decision/src/Model/MeetingMinutes.php b/module/Decision/src/Model/MeetingMinutes.php
index 0fbd385b60..bfc8376ad5 100644
--- a/module/Decision/src/Model/MeetingMinutes.php
+++ b/module/Decision/src/Model/MeetingMinutes.php
@@ -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;
@@ -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;
/**
diff --git a/module/Decision/src/Model/SubDecision.php b/module/Decision/src/Model/SubDecision.php
index 12e356d247..579367097c 100644
--- a/module/Decision/src/Model/SubDecision.php
+++ b/module/Decision/src/Model/SubDecision.php
@@ -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;
@@ -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;
/**
diff --git a/module/Decision/src/Model/SubDecision/Installation.php b/module/Decision/src/Model/SubDecision/Installation.php
index 667cc904f7..61f68d8930 100644
--- a/module/Decision/src/Model/SubDecision/Installation.php
+++ b/module/Decision/src/Model/SubDecision/Installation.php
@@ -58,7 +58,7 @@ class Installation extends FoundationReference
targetEntity: Discharge::class,
mappedBy: 'installation',
)]
- protected Discharge $discharge;
+ protected ?Discharge $discharge = null;
/**
* The organmember reference.
@@ -114,7 +114,7 @@ public function getReappointments(): Collection
/**
* Get the discharge, if it exists.
*/
- public function getDischarge(): Discharge
+ public function getDischarge(): ?Discharge
{
return $this->discharge;
}
diff --git a/module/Decision/test/Seeder/DecisionFixture.php b/module/Decision/test/Seeder/DecisionFixture.php
new file mode 100644
index 0000000000..ca3a07c322
--- /dev/null
+++ b/module/Decision/test/Seeder/DecisionFixture.php
@@ -0,0 +1,274 @@
+setMeeting($this->getReference('meeting-BV-0', Meeting::class));
+ $decision->setPoint(1);
+ $decision->setNumber(1);
+ $decision->setContent('');
+
+ $manager->persist($decision);
+ $this->addReference('decision-BV-0-' . $decision->getPoint() . '-' . $decision->getNumber(), $decision);
+
+ $sequence = 1;
+ $iSubdecisions = [];
+
+ $foundation = new Foundation();
+ $foundation->setAbbr('GETÉST');
+ $foundation->setName('GEWIS\'ers Testen Éigenlijk Structureel Te-weinig');
+ $foundation->setOrganType(OrganTypes::Committee);
+ $foundation->setDecision($decision);
+ $foundation->setSequence($sequence);
+ $foundation->setContent(sprintf(
+ '%s %s met afkorting %s wordt opgericht.',
+ ucfirst($foundation->getOrganType()->value), // shortcut as getting the translator for `getName()` sucks.
+ $foundation->getName(),
+ $foundation->getAbbr(),
+ ));
+
+ $manager->persist($foundation);
+ $iSubdecisions[] = $foundation;
+ $this->addReference('foundation-' . $foundation->getSequence(), $foundation);
+
+ foreach (range(8005, 8024) as $lidnr) {
+ $sequence++;
+ $iSubdecisions[] = $this->createInstallation(
+ 'Lid',
+ $lidnr,
+ $sequence,
+ $foundation,
+ $decision,
+ $manager,
+ );
+
+ // Additional roles for specific members.
+ if (8005 === $lidnr) {
+ $sequence++;
+ $iSubdecisions[] = $this->createInstallation(
+ 'Voorzitter',
+ $lidnr,
+ $sequence,
+ $foundation,
+ $decision,
+ $manager,
+ );
+ }
+
+ if (8006 === $lidnr) {
+ $sequence++;
+ $iSubdecisions[] = $this->createInstallation(
+ 'Secretaris',
+ $lidnr,
+ $sequence,
+ $foundation,
+ $decision,
+ $manager,
+ );
+ }
+
+ // Will be discharged.
+ if (8020 === $lidnr) {
+ $sequence++;
+ $iSubdecisions[] = $this->createInstallation(
+ 'Penningmeester',
+ $lidnr,
+ $sequence,
+ $foundation,
+ $decision,
+ $manager,
+ );
+ }
+ }
+
+ $decision->addSubdecisions($iSubdecisions);
+ $content = [];
+
+ foreach ($decision->getSubdecisions() as $subdecision) {
+ $content[] = $subdecision->getContent();
+ }
+
+ $decision->setContent(implode(' ', $content));
+ $manager->persist($decision);
+
+ $manager->flush();
+
+ // Discharge of members of GETEST
+ $decision = new Decision();
+ $decision->setMeeting($this->getReference('meeting-BV-1', Meeting::class));
+ $decision->setPoint(1);
+ $decision->setNumber(1);
+ $decision->setContent('');
+
+ $manager->persist($decision);
+ $this->addReference('decision-BV-1-' . $decision->getPoint() . '-' . $decision->getNumber(), $decision);
+
+ $sequence = 1;
+ $dSubdecisions = [];
+
+ foreach (range(8020, 8024) as $lidnr) {
+ // Order of discharge matters, the discharge from a special function comes before `Lid`.
+ if (8020 === $lidnr) {
+ $dSubdecisions[] = $this->createDischarge(
+ $sequence,
+ $sequence + 18, // TODO: find a better way to calculate this.
+ $decision,
+ $manager,
+ );
+ $sequence++;
+ }
+
+ $dSubdecisions[] = $this->createDischarge(
+ $sequence,
+ $sequence + 18, // TODO: find a better way to calculate this.
+ $decision,
+ $manager,
+ );
+ $sequence++;
+ }
+
+ $decision->addSubdecisions($dSubdecisions);
+ $content = [];
+
+ foreach ($decision->getSubdecisions() as $dSubdecision) {
+ $content[] = $dSubdecision->getContent();
+ }
+
+ $decision->setContent(implode(' ', $content));
+ $manager->persist($decision);
+
+ $manager->flush();
+
+ // Creation of the actual organ and its members here as well. This is because Doctrine sucks and breaks in the
+ // opposite way with the custom mapping type.
+
+ // Foundation
+ $organ = new Organ();
+ $organ->setName($foundation->getName());
+ $organ->setAbbr($foundation->getAbbr());
+ $organ->setFoundation($foundation);
+ $organ->setType($foundation->getOrganType());
+ $organ->setFoundationDate($foundation->getDecision()->getMeeting()->getDate());
+
+ $manager->persist($organ);
+ $manager->flush();
+
+ // Installations
+ foreach ($iSubdecisions as $installation) {
+ if (!($installation instanceof Installation)) {
+ continue;
+ }
+
+ $organMember = new OrganMember();
+ $organMember->setOrgan($organ);
+ $organMember->setMember($installation->getMember());
+ $organMember->setInstallation($installation);
+ $organMember->setFunction($installation->getFunction());
+ $organMember->setInstallDate($installation->getFoundation()->getDecision()->getMeeting()->getDate());
+
+ $manager->persist($organMember);
+ $this->addReference('organMember-' . $installation->getSequence(), $organMember);
+ }
+
+ $manager->flush();
+
+ // Discharges
+ foreach ($dSubdecisions as $discharge) {
+ $organMember = $this->getReference('organMember-' . $discharge->getInstallation()->getSequence(), OrganMember::class);
+ $organMember->setDischargeDate($discharge->getDecision()->getMeeting()->getDate());
+
+ $manager->persist($organMember);
+ }
+
+ $manager->flush();
+ }
+
+ private function createInstallation(
+ string $function,
+ int $lidnr,
+ int $sequence,
+ Foundation $foundation,
+ Decision $decision,
+ ObjectManager $manager,
+ ): Installation {
+ $installation = new Installation();
+ $installation->setFunction($function);
+ $installation->setMember($this->getReference('member-' . $lidnr, Member::class));
+ $installation->setSequence($sequence);
+ $installation->setFoundation($foundation);
+ $installation->setDecision($decision);
+ $installation->setContent(
+ sprintf(
+ '%s wordt geïnstalleerd als %s van %s',
+ $installation->getMember()->getFullName(),
+ $installation->getFunction(),
+ $installation->getFoundation()->getAbbr(),
+ )
+ );
+
+ $manager->persist($installation);
+ $this->addReference('installation-' . $installation->getSequence(), $installation);
+
+ return $installation;
+ }
+
+ private function createDischarge(
+ int $sequence,
+ int $installationSequence,
+ Decision $decision,
+ ObjectManager $manager,
+ ): Discharge {
+ $discharge = new Discharge();
+ $discharge->setInstallation($this->getReference('installation-' . $installationSequence, Installation::class));
+ $discharge->setSequence($sequence);
+ $discharge->setDecision($decision);
+ $discharge->setContent(
+ sprintf(
+ '%s wordt gedechargeerd als %s van %s',
+ $discharge->getInstallation()->getMember()->getFullName(),
+ $discharge->getInstallation()->getFunction(),
+ $discharge->getInstallation()->getFoundation()->getAbbr(),
+ ),
+ );
+
+ $manager->persist($discharge);
+ $this->addReference('discharge-' . $discharge->getSequence(), $discharge);
+
+ return $discharge;
+ }
+
+ /**
+ * @return class-string[]
+ */
+ public function getDependencies(): array
+ {
+ return [
+ MeetingFixture::class,
+ ];
+ }
+}
diff --git a/module/Decision/test/Seeder/MeetingFixture.php b/module/Decision/test/Seeder/MeetingFixture.php
new file mode 100644
index 0000000000..4808b6d1dd
--- /dev/null
+++ b/module/Decision/test/Seeder/MeetingFixture.php
@@ -0,0 +1,44 @@
+setType($meetingType);
+ $meeting->setNumber($meetingNumber);
+
+ // 2 meetings in the past, 1 today, and 1 in the future.
+ if (3 > $meetingNumber) {
+ $meetingDate = (clone $today)->modify("-" . (2 - $meetingNumber) . " days");
+ } else {
+ $meetingDate = AssociationYear::fromDate($today)->getEndDate();
+ }
+
+ $meeting->setDate($meetingDate);
+
+ $manager->persist($meeting);
+ $this->addReference('meeting-' . $meetingType->value . '-' . $meetingNumber, $meeting);
+ }
+
+ $manager->flush();
+ }
+ }
+}
diff --git a/module/Decision/test/Seeder/MemberFixture.php b/module/Decision/test/Seeder/MemberFixture.php
new file mode 100644
index 0000000000..8974b8804a
--- /dev/null
+++ b/module/Decision/test/Seeder/MemberFixture.php
@@ -0,0 +1,275 @@
+faker = FakerFactory::create();
+ $this->now = new DateTimeImmutable();
+
+ // Admins (8000 - 8002)
+ foreach (range(8000, 8002) as $lidnr) {
+ $admin = new Member();
+ $admin->setLidnr($lidnr);
+ $admin->setFirstName('ÅDMIN');
+
+ $admin = $this->setOtherMemberProperties(
+ $admin,
+ MembershipTypes::Ordinary,
+ false,
+ false,
+ false,
+ );
+
+ $manager->persist($admin);
+ $this->addReference('member-' . $lidnr, $admin);
+ }
+
+ $manager->flush();
+
+ // Company admins (8003 - 8004)
+ foreach (range(8003, 8004) as $lidnr) {
+ $companyAdmin = new Member();
+ $companyAdmin->setLidnr($lidnr);
+ $companyAdmin->setFirstName('COMPANY_ADMIN');
+
+ $companyAdmin = $this->setOtherMemberProperties(
+ $companyAdmin,
+ MembershipTypes::Ordinary,
+ false,
+ false,
+ false,
+ );
+
+ $manager->persist($companyAdmin);
+ $this->addReference('member-' . $lidnr, $companyAdmin);
+ }
+
+ $manager->flush();
+
+ // Active (ordinary) members (8005 - 8014)
+ foreach (range(8005, 8014) as $lidnr) {
+ $member = new Member();
+ $member->setLidnr($lidnr);
+ $member->setFirstName('ORGAN_ORDINARY');
+
+ $member = $this->setOtherMemberProperties(
+ $member,
+ MembershipTypes::Ordinary,
+ false,
+ false,
+ false,
+ );
+
+ $manager->persist($member);
+ $this->addReference('member-' . $lidnr, $member);
+ }
+
+ $manager->flush();
+
+ // Active (external) members (8015 - 8019)
+ foreach (range(8015, 8019) as $lidnr) {
+ $member = new Member();
+ $member->setLidnr($lidnr);
+ $member->setFirstName('ORGAN_EXTERNAL');
+
+ $member = $this->setOtherMemberProperties(
+ $member,
+ MembershipTypes::External,
+ false,
+ false,
+ false,
+ );
+
+ $manager->persist($member);
+ $this->addReference('member-' . $lidnr, $member);
+ }
+
+ $manager->flush();
+
+ // Discharged active members (8020 - 8024)
+ foreach (range(8020, 8024) as $lidnr) {
+ $member = new Member();
+ $member->setLidnr($lidnr);
+ $member->setFirstName('ORGAN_DISCHARGED');
+
+ $member = $this->setOtherMemberProperties(
+ $member,
+ MembershipTypes::Ordinary,
+ false,
+ false,
+ false,
+ );
+
+ $manager->persist($member);
+ $this->addReference('member-' . $lidnr, $member);
+ }
+
+ $manager->flush();
+
+ // Ordinary members (8025 - 8124)
+ foreach (range(8025, 8124) as $lidnr) {
+ $member = new Member();
+ $member->setLidnr($lidnr);
+ $member->setFirstName($this->faker->firstName());
+
+ $member = $this->setOtherMemberProperties(
+ $member,
+ MembershipTypes::Ordinary,
+ false,
+ false,
+ false,
+ );
+
+ $manager->persist($member);
+ $this->addReference('member-' . $lidnr, $member);
+ }
+
+ $manager->flush();
+
+ // External members (8125 - 8149)
+ foreach (range(8125, 8149) as $lidnr) {
+ $external = new Member();
+ $external->setLidnr($lidnr);
+ $external->setFirstName($this->faker->firstName());
+
+ $external = $this->setOtherMemberProperties(
+ $external,
+ MembershipTypes::External,
+ false,
+ false,
+ false,
+ );
+
+ $manager->persist($external);
+ $this->addReference('member-' . $lidnr, $external);
+ }
+
+ $manager->flush();
+
+ // Honorary members (8150 - 8154)
+ foreach (range(8150, 8154) as $lidnr) {
+ $honorary = new Member();
+ $honorary->setLidnr($lidnr);
+ $honorary->setFirstName($this->faker->firstName());
+
+ $honorary = $this->setOtherMemberProperties(
+ $honorary,
+ MembershipTypes::Honorary,
+ false,
+ false,
+ false,
+ );
+
+ $manager->persist($honorary);
+ $this->addReference('member-' . $lidnr, $honorary);
+ }
+
+ $manager->flush();
+
+ // Graduates (8155 - 8199)
+ foreach (range(8155, 8199) as $lidnr) {
+ $graduate = new Member();
+ $graduate->setLidnr($lidnr);
+ $graduate->setFirstName($this->faker->firstName());
+
+ $graduate = $this->setOtherMemberProperties(
+ $graduate,
+ MembershipTypes::Graduate,
+ $this->faker->boolean(),
+ false,
+ false,
+ );
+
+ $manager->persist($graduate);
+ $this->addReference('member-' . $lidnr, $graduate);
+ }
+
+ $manager->flush();
+ }
+
+ private function setOtherMemberProperties(
+ Member $member,
+ MembershipTypes $membershipType,
+ bool $expired,
+ bool $hidden = false,
+ bool $deleted = false,
+ ): Member {
+ $member->setInitials(
+ implode(
+ '.',
+ array_map(
+ static function ($name) {
+ return mb_substr($name, 0, 1);
+ },
+ explode(
+ ' ',
+ $member->getFirstName(),
+ ),
+ ),
+ ) . '.',
+ );
+ $member->setMiddleName('');
+ $member->setLastName($this->faker->lastName());
+
+ $member->setEmail($this->faker->email());
+ $member->setBirth($this->faker->dateTimeThisCentury('-16 years'));
+
+ $member->setGeneration(intval($this->faker->year()));
+ $member->setType($membershipType);
+
+ if (!$expired) {
+ // If not expired, expire next year.
+ $member->setExpiration(
+ AssociationYear::fromDate(
+ DateTime::createFromImmutable($this->now->add(new DateInterval('P1Y'))),
+ )->getStartDate(),
+ );
+
+ if (MembershipTypes::Ordinary === $membershipType) {
+ $member->setMembershipEndsOn(null);
+ } else {
+ $member->setMembershipEndsOn($member->getExpiration());
+ }
+ } else {
+ $member->setExpiration(
+ AssociationYear::fromDate(
+ DateTime::createFromImmutable($this->now->sub(new DateInterval('P2Y'))),
+ )->getStartDate(),
+ );
+ $member->setMembershipEndsOn($member->getExpiration()->sub(new DateInterval('P1Y')));
+ }
+
+ $member->setHidden($hidden);
+ $member->setDeleted($deleted);
+ $member->setAuthenticationKey($expired ? null : $this->faker->sha256());
+ $member->setChangedOn(DateTime::createFromImmutable($this->now));
+
+ return $member;
+ }
+}
diff --git a/module/User/test/Seeder/UserFixture.php b/module/User/test/Seeder/UserFixture.php
new file mode 100644
index 0000000000..5b8640fc8e
--- /dev/null
+++ b/module/User/test/Seeder/UserFixture.php
@@ -0,0 +1,44 @@
+setLidnr($lidnr);
+ $user->setMember($this->getReference('member-' . $lidnr, Member::class));
+ $user->setPassword('$2y$13$j.ggomvkEeev1tcrsg7tEObJdD0LGQpmfT/4k8zwclvyFM5zFxkde'); // == gewiswebgewis
+ $user->setPasswordChangedOn(new DateTime());
+
+ $manager->persist($user);
+ $this->addReference('user-' . $lidnr, $user);
+ }
+
+ $manager->flush();
+ }
+
+ /**
+ * @return class-string[]
+ */
+ public function getDependencies(): array
+ {
+ return [
+ MemberFixture::class,
+ ];
+ }
+}
diff --git a/module/User/test/Seeder/UserRoleFixture.php b/module/User/test/Seeder/UserRoleFixture.php
new file mode 100644
index 0000000000..37669e1164
--- /dev/null
+++ b/module/User/test/Seeder/UserRoleFixture.php
@@ -0,0 +1,54 @@
+setRole(UserRoles::Admin);
+ $adminRole->setExpiration((new DateTime())->add(new DateInterval('P10Y')));
+ $adminRole->setLidnr($this->getReference('user-' . $lidnr, User::class));
+
+ $manager->persist($adminRole);
+ }
+
+ // Company admins (8003 - 8004)
+ foreach (range(8003, 8004) as $lidnr) {
+ $companyAdminRole = new UserRole();
+ $companyAdminRole->setRole(UserRoles::CompanyAdmin);
+ $companyAdminRole->setExpiration((new DateTime())->add(new DateInterval('P10Y')));
+ $companyAdminRole->setLidnr($this->getReference('user-' . $lidnr, User::class));
+
+ $manager->persist($companyAdminRole);
+ }
+
+ $manager->flush();
+ }
+
+ /**
+ * @return class-string[]
+ */
+ public function getDependencies(): array
+ {
+ return [
+ UserFixture::class,
+ ];
+ }
+}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 6ee29c0bf5..783498db45 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -44,6 +44,7 @@
module/Activity/src/Controller/Factory/ActivityControllerFactory.php
module/Activity/src/Controller/Factory/AdminCategoryControllerFactory.php
module/Activity/src/Controller/Factory/AdminOptionControllerFactory.php
+ module/Application/src/Command/Factory/LoadFixturesFactory.php
module/Application/src/Router/Factory/LanguageAwareTreeRouteStackFactory.php
module/Education/src/Controller/Factory/AdminControllerFactory.php
module/Education/src/Controller/Factory/EducationControllerFactory.php