Skip to content

Commit

Permalink
Merge pull request #43 from wmde/doctrine-update-2024
Browse files Browse the repository at this point in the history
Update Doctrine ORM and DBAL
  • Loading branch information
Sperling-0 authored Feb 28, 2024
2 parents c73e003 + 9290c31 commit 0a00da4
Show file tree
Hide file tree
Showing 22 changed files with 245 additions and 126 deletions.
42 changes: 42 additions & 0 deletions bin/doctrine
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env php
<?php

use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Tools\DsnParser;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use Symfony\Component\Dotenv\Dotenv;
use WMDE\Fundraising\AddressChangeContext\AddressChangeContextFactory;

require __DIR__.'/../vendor/autoload.php';

$dotenv = new Dotenv();
$dotenv->load( __DIR__ . '/../.env' );

function createEntityManager(): EntityManager {
if (empty( $_ENV['DB_DSN'] ) ) {
echo "You must set the database connection string in 'DB_DSN'\n";
exit(1);
}
$dsnParser = new DsnParser(['mysql' => 'pdo_mysql']);
$connectionParams = $dsnParser
->parse( $_ENV['DB_DSN'] );
$connection = DriverManager::getConnection( $connectionParams );

$contextFactory = new AddressChangeContextFactory();
$contextFactory->registerCustomTypes( $connection );
$doctrineConfig = ORMSetup::createXMLMetadataConfiguration(
$contextFactory->getDoctrineMappingPaths(),
true
);

return new EntityManager( $connection, $doctrineConfig );
}


ConsoleRunner::run(
new SingleManagerProvider(createEntityManager()),
[]
);
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
"license": "GPL-2.0-or-later",
"require": {
"php": ">=8.1",
"doctrine/orm": "^2.11",
"doctrine/dbal": "^3.3",
"doctrine/orm": "~2.18 | ~3.0",
"doctrine/dbal": "~3.8 | ~4.0",
"ramsey/uuid": "^4.0",
"wmde/freezable-value-object": "~2.0"
},
"require-dev": {
"phpunit/phpunit": "~9.2",
"symfony/cache": "^5.3",
"symfony/dotenv": "~6.4",
"wmde/fundraising-phpcs": "~10.0",
"phpmd/phpmd": "~2.6",
"phpstan/phpstan": "^1.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
<embedded name="identifier" class="WMDE\Fundraising\AddressChangeContext\Domain\Model\AddressChangeId" column-prefix="current_" />
<embedded name="previousIdentifier" class="AddressChangeId" column-prefix="previous_" />

<field name="addressType" type="string" column="address_type" length="10" nullable="false" />
<field name="addressType" type="AddressType" column="address_type" length="10" nullable="false" />
<field name="externalId" type="integer" column="external_id" nullable="false" />
<field name="externalIdType" type="string" column="external_id_type" length="10" nullable="false" />
<field name="exportDate" type="datetime" column="export_date" nullable="true" />
<field name="createdAt" type="datetime" column="created_at" nullable="false" />
<field name="modifiedAt" type="datetime" column="modified_at" nullable="false" />
<field name="donationReceipt" type="boolean" column="donation_receipt" nullable="false" />

<many-to-one field="address" target-entity="Address" fetch="LAZY">
<many-to-one field="address" target-entity="Address" fetch="EAGER">
<join-columns>
<join-column name="address_id" referenced-column-name="id"/>
</join-columns>
Expand Down
15 changes: 15 additions & 0 deletions src/AddressChangeContextFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace WMDE\Fundraising\AddressChangeContext;

use Doctrine\Common\EventSubscriber;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Types\Type;

/**
* @license GPL-2.0-or-later
Expand Down Expand Up @@ -34,4 +36,17 @@ public function newEventSubscribers(): array {
return [];
}

public function registerCustomTypes( Connection $connection ): void {
$this->registerDoctrinePaymentIntervalType( $connection );
}

public function registerDoctrinePaymentIntervalType( Connection $connection ): void {
static $isRegistered = false;
if ( $isRegistered ) {
return;
}
Type::addType( 'AddressType', 'WMDE\Fundraising\AddressChangeContext\DataAccess\DoctrineTypes\AddressType' );
$connection->getDatabasePlatform()->registerDoctrineTypeMapping( 'AddressType', 'AddressType' );
$isRegistered = true;
}
}
6 changes: 2 additions & 4 deletions src/DataAccess/DoctrineAddressChangeRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@ public function getAddressChangeByUuids( string $currentIdentifier, string $prev
return $this->entityManager->getRepository( AddressChange::class )->createQueryBuilder( 'ac' )
->where( 'ac.identifier.identifier = :currentIdentifier' )
->orWhere( 'ac.previousIdentifier.identifier = :previousIdentifier' )
->setParameters( [
'currentIdentifier' => $currentIdentifier,
'previousIdentifier' => $previousIdentifier
] )
->setParameter( 'currentIdentifier', $currentIdentifier )
->setParameter( 'previousIdentifier', $previousIdentifier )
->getQuery()
->getOneOrNullResult();
}
Expand Down
42 changes: 42 additions & 0 deletions src/DataAccess/DoctrineTypes/AddressType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace WMDE\Fundraising\AddressChangeContext\DataAccess\DoctrineTypes;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use WMDE\Fundraising\AddressChangeContext\Domain\Model\AddressType as DomainAddressType;

class AddressType extends Type {
public function getSQLDeclaration( array $column, AbstractPlatform $platform ): string {
return 'VARCHAR(10)';
}

public function convertToPHPValue( mixed $value, AbstractPlatform $platform ): DomainAddressType {
return match ( $value ) {
'person' => DomainAddressType::Person,
'company' => DomainAddressType::Company,
default => throw new \InvalidArgumentException(
"Could not convert address type string ({$value}) to enum"
),
};
}

public function convertToDatabaseValue( mixed $value, AbstractPlatform $platform ): string {
return match ( $value ) {
DomainAddressType::Person => 'person',
DomainAddressType::Company => 'company',
default => throw new \InvalidArgumentException(
"Could not convert address type enum ({$value}) to string"
),
};
}

/**
* @codeCoverageIgnore
* @return string
*/
public function getName(): string {
return 'AddressType';
}

}
23 changes: 4 additions & 19 deletions src/Domain/Model/Address.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@

class Address {

private const TYPE_PERSONAL = 'personal';

private const TYPE_COMPANY = 'company';

/**
* @var int|null
* @phpstan-ignore-next-line
Expand All @@ -36,8 +32,6 @@ class Address {

private string $country = '';

private string $addressType;

private function __construct(
string $salutation,
string $company,
Expand All @@ -47,8 +41,8 @@ private function __construct(
string $address,
string $postcode,
string $city,
string $country,
string $addressType ) {
string $country
) {
$this->salutation = $salutation;
$this->company = $company;
$this->title = $title;
Expand All @@ -58,7 +52,6 @@ private function __construct(
$this->postcode = $postcode;
$this->city = $city;
$this->country = $country;
$this->addressType = $addressType;
}

public static function newPersonalAddress(
Expand All @@ -78,7 +71,7 @@ public static function newPersonalAddress(
self::assertNotEmpty( 'City', $city );
self::assertNotEmpty( 'Country', $country );

return new self( $salutation, '', $title, $firstName, $lastName, $address, $postcode, $city, $country, self::TYPE_PERSONAL );
return new self( $salutation, '', $title, $firstName, $lastName, $address, $postcode, $city, $country );
}

public static function newCompanyAddress(
Expand All @@ -92,7 +85,7 @@ public static function newCompanyAddress(
self::assertNotEmpty( 'Post Code', $postcode );
self::assertNotEmpty( 'City', $city );
self::assertNotEmpty( 'Country', $country );
return new self( '', $company, '', '', '', $address, $postcode, $city, $country, self::TYPE_COMPANY );
return new self( '', $company, '', '', '', $address, $postcode, $city, $country );
}

private static function assertNotEmpty( string $field, string $value ): void {
Expand All @@ -101,14 +94,6 @@ private static function assertNotEmpty( string $field, string $value ): void {
}
}

public function isPersonalAddress(): bool {
return $this->addressType === self::TYPE_PERSONAL;
}

public function isCompanyAddress(): bool {
return $this->addressType === self::TYPE_COMPANY;
}

public function getSalutation(): string {
return $this->salutation;
}
Expand Down
36 changes: 10 additions & 26 deletions src/Domain/Model/AddressChange.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@
* The recommended way to construct this class is through the AddressChangeBuilder
*/
class AddressChange {

public const ADDRESS_TYPE_PERSON = 'person';
public const ADDRESS_TYPE_COMPANY = 'company';

public const EXTERNAL_ID_TYPE_DONATION = 'donation';
public const EXTERNAL_ID_TYPE_MEMBERSHIP = 'membership';

Expand All @@ -30,44 +26,32 @@ class AddressChange {
*/
private ?int $id;

private AddressChangeId $identifier;

private AddressChangeId $previousIdentifier;

private ?Address $address;

private string $addressType;

private bool $donationReceipt;

private int $externalId;

private string $externalIdType;

private ?\DateTimeInterface $exportDate;

private \DateTimeInterface $createdAt;

private \DateTimeInterface $modifiedAt;

public function __construct( string $addressType, string $externalIdType, int $externalId, AddressChangeId $identifier,
?Address $address = null, ?\DateTime $createdAt = null ) {
$this->addressType = $addressType;
$this->identifier = $identifier;
public function __construct(
private readonly AddressType $addressType,
private readonly string $externalIdType,
private readonly int $externalId,
private AddressChangeId $identifier,
private ?Address $address = null,
?\DateTime $createdAt = null
) {
$this->previousIdentifier = $identifier;
$this->address = $address;
if ( $addressType !== self::ADDRESS_TYPE_PERSON && $addressType !== self::ADDRESS_TYPE_COMPANY ) {
throw new \InvalidArgumentException( 'Invalid address type' );
}
if ( $externalIdType !== self::EXTERNAL_ID_TYPE_DONATION && $externalIdType !== self::EXTERNAL_ID_TYPE_MEMBERSHIP ) {
throw new \InvalidArgumentException( 'Invalid external reference type' );
}
$this->exportDate = null;
$this->createdAt = $createdAt ?? new \DateTime();
$this->modifiedAt = clone $this->createdAt;
$this->donationReceipt = true;
$this->externalId = $externalId;
$this->externalIdType = $externalIdType;
}

public function performAddressChange( Address $address, AddressChangeId $newIdentifier ): void {
Expand Down Expand Up @@ -96,11 +80,11 @@ public function getAddress(): ?Address {
}

public function isPersonalAddress(): bool {
return $this->addressType === self::ADDRESS_TYPE_PERSON;
return $this->addressType === AddressType::Person;
}

public function isCompanyAddress(): bool {
return $this->addressType === self::ADDRESS_TYPE_COMPANY;
return $this->addressType === AddressType::Company;
}

public function isOptedIntoDonationReceipt(): bool {
Expand Down
8 changes: 4 additions & 4 deletions src/Domain/Model/AddressChangeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class AddressChangeBuilder {

private static ?UuidGenerator $uuidGenerator = null;

private ?string $addressType;
private ?AddressType $addressType;
private ?string $referenceType;
private ?int $referenceId;
private AddressChangeId $identifier;
Expand All @@ -34,14 +34,14 @@ public static function create( ?AddressChangeId $identifier = null, ?Address $ad
}

public function forPerson(): self {
return $this->setAddressType( AddressChange::ADDRESS_TYPE_PERSON );
return $this->setAddressType( AddressType::Person );
}

public function forCompany(): self {
return $this->setAddressType( AddressChange::ADDRESS_TYPE_COMPANY );
return $this->setAddressType( AddressType::Company );
}

private function setAddressType( string $addressType ): self {
public function setAddressType( AddressType $addressType ): self {
if ( $this->addressType !== null ) {
throw new \RuntimeException( 'You can only specify address type once' );
}
Expand Down
9 changes: 9 additions & 0 deletions src/Domain/Model/AddressType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
declare( strict_types=1 );

namespace WMDE\Fundraising\AddressChangeContext\Domain\Model;

enum AddressType {
case Person;
case Company;
}
33 changes: 33 additions & 0 deletions src/ScalarTypeConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
declare( strict_types=1 );

namespace WMDE\Fundraising\AddressChangeContext;

/**
* This class converts "mixed" values from libraries like Doctrine or Symfony into scalar values, without tripping up
* PHPStan, which disallows calling "strval" and "intval" on variables with "mixed" type, because calling them
* with objects or arrays will generate a warning.
*
* DO NOT USE THIS IN LOOPS! (E.g. iterating over database results).
* The constant type checking will slow down the application, use PHPStan-specific comments to ignore this error instead:
* https://phpstan.org/user-guide/ignoring-errors
*
* Hopefully, in the future libraries will return fewer "mixed" types. Please check from time to time if this class is still needed.
* Check the usage of the class methods to detect libraries that return mixed.
*/
class ScalarTypeConverter {
public static function toInt( mixed $value ): int {
return intval( self::assertScalarType( $value ) );
}

public static function toString( mixed $value ): string {
return strval( self::assertScalarType( $value ) );
}

private static function assertScalarType( mixed $value ): int|string|bool|float {
if ( is_scalar( $value ) ) {
return $value;
}
throw new \InvalidArgumentException( "Given value is not a scalar type" );
}
}
Loading

0 comments on commit 0a00da4

Please sign in to comment.