Skip to content

Commit 9290c31

Browse files
committed
Add AddressType enum
Ticket: https://phabricator.wikimedia.org/T356744
1 parent 8c8f3ce commit 9290c31

16 files changed

+215
-90
lines changed

bin/doctrine

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
use Doctrine\DBAL\DriverManager;
5+
use Doctrine\DBAL\Tools\DsnParser;
6+
use Doctrine\ORM\EntityManager;
7+
use Doctrine\ORM\ORMSetup;
8+
use Doctrine\ORM\Tools\Console\ConsoleRunner;
9+
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
10+
use Symfony\Component\Dotenv\Dotenv;
11+
use WMDE\Fundraising\AddressChangeContext\AddressChangeContextFactory;
12+
13+
require __DIR__.'/../vendor/autoload.php';
14+
15+
$dotenv = new Dotenv();
16+
$dotenv->load( __DIR__ . '/../.env' );
17+
18+
function createEntityManager(): EntityManager {
19+
if (empty( $_ENV['DB_DSN'] ) ) {
20+
echo "You must set the database connection string in 'DB_DSN'\n";
21+
exit(1);
22+
}
23+
$dsnParser = new DsnParser(['mysql' => 'pdo_mysql']);
24+
$connectionParams = $dsnParser
25+
->parse( $_ENV['DB_DSN'] );
26+
$connection = DriverManager::getConnection( $connectionParams );
27+
28+
$contextFactory = new AddressChangeContextFactory();
29+
$contextFactory->registerCustomTypes( $connection );
30+
$doctrineConfig = ORMSetup::createXMLMetadataConfiguration(
31+
$contextFactory->getDoctrineMappingPaths(),
32+
true
33+
);
34+
35+
return new EntityManager( $connection, $doctrineConfig );
36+
}
37+
38+
39+
ConsoleRunner::run(
40+
new SingleManagerProvider(createEntityManager()),
41+
[]
42+
);

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "Address change use case for fundraising application",
44
"license": "GPL-2.0-or-later",
55
"require": {
6-
"php": ">=8.0",
6+
"php": ">=8.1",
77
"doctrine/orm": "~2.18 | ~3.0",
88
"doctrine/dbal": "~3.8 | ~4.0",
99
"ramsey/uuid": "^4.0",
@@ -12,6 +12,7 @@
1212
"require-dev": {
1313
"phpunit/phpunit": "~9.2",
1414
"symfony/cache": "^5.3",
15+
"symfony/dotenv": "~6.4",
1516
"wmde/fundraising-phpcs": "~10.0",
1617
"phpmd/phpmd": "~2.6",
1718
"phpstan/phpstan": "^1.2",

config/DoctrineClassMapping/WMDE.Fundraising.AddressChangeContext.Domain.Model.AddressChange.dcm.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<embedded name="identifier" class="WMDE\Fundraising\AddressChangeContext\Domain\Model\AddressChangeId" column-prefix="current_" />
1616
<embedded name="previousIdentifier" class="AddressChangeId" column-prefix="previous_" />
1717

18-
<field name="addressType" type="string" column="address_type" length="10" nullable="false" />
18+
<field name="addressType" type="AddressType" column="address_type" length="10" nullable="false" />
1919
<field name="externalId" type="integer" column="external_id" nullable="false" />
2020
<field name="externalIdType" type="string" column="external_id_type" length="10" nullable="false" />
2121
<field name="exportDate" type="datetime" column="export_date" nullable="true" />

src/AddressChangeContextFactory.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace WMDE\Fundraising\AddressChangeContext;
66

77
use Doctrine\Common\EventSubscriber;
8+
use Doctrine\DBAL\Connection;
9+
use Doctrine\DBAL\Types\Type;
810

911
/**
1012
* @license GPL-2.0-or-later
@@ -34,4 +36,17 @@ public function newEventSubscribers(): array {
3436
return [];
3537
}
3638

39+
public function registerCustomTypes( Connection $connection ): void {
40+
$this->registerDoctrinePaymentIntervalType( $connection );
41+
}
42+
43+
public function registerDoctrinePaymentIntervalType( Connection $connection ): void {
44+
static $isRegistered = false;
45+
if ( $isRegistered ) {
46+
return;
47+
}
48+
Type::addType( 'AddressType', 'WMDE\Fundraising\AddressChangeContext\DataAccess\DoctrineTypes\AddressType' );
49+
$connection->getDatabasePlatform()->registerDoctrineTypeMapping( 'AddressType', 'AddressType' );
50+
$isRegistered = true;
51+
}
3752
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace WMDE\Fundraising\AddressChangeContext\DataAccess\DoctrineTypes;
4+
5+
use Doctrine\DBAL\Platforms\AbstractPlatform;
6+
use Doctrine\DBAL\Types\Type;
7+
use WMDE\Fundraising\AddressChangeContext\Domain\Model\AddressType as DomainAddressType;
8+
9+
class AddressType extends Type {
10+
public function getSQLDeclaration( array $column, AbstractPlatform $platform ): string {
11+
return 'VARCHAR(10)';
12+
}
13+
14+
public function convertToPHPValue( mixed $value, AbstractPlatform $platform ): DomainAddressType {
15+
return match ( $value ) {
16+
'person' => DomainAddressType::Person,
17+
'company' => DomainAddressType::Company,
18+
default => throw new \InvalidArgumentException(
19+
"Could not convert address type string ({$value}) to enum"
20+
),
21+
};
22+
}
23+
24+
public function convertToDatabaseValue( mixed $value, AbstractPlatform $platform ): string {
25+
return match ( $value ) {
26+
DomainAddressType::Person => 'person',
27+
DomainAddressType::Company => 'company',
28+
default => throw new \InvalidArgumentException(
29+
"Could not convert address type enum ({$value}) to string"
30+
),
31+
};
32+
}
33+
34+
/**
35+
* @codeCoverageIgnore
36+
* @return string
37+
*/
38+
public function getName(): string {
39+
return 'AddressType';
40+
}
41+
42+
}

src/Domain/Model/AddressChange.php

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@
1313
* The recommended way to construct this class is through the AddressChangeBuilder
1414
*/
1515
class AddressChange {
16-
17-
public const ADDRESS_TYPE_PERSON = 'person';
18-
public const ADDRESS_TYPE_COMPANY = 'company';
19-
2016
public const EXTERNAL_ID_TYPE_DONATION = 'donation';
2117
public const EXTERNAL_ID_TYPE_MEMBERSHIP = 'membership';
2218

@@ -30,44 +26,32 @@ class AddressChange {
3026
*/
3127
private ?int $id;
3228

33-
private AddressChangeId $identifier;
34-
3529
private AddressChangeId $previousIdentifier;
3630

37-
private ?Address $address;
38-
39-
private string $addressType;
40-
4131
private bool $donationReceipt;
4232

43-
private int $externalId;
44-
45-
private string $externalIdType;
46-
4733
private ?\DateTimeInterface $exportDate;
4834

4935
private \DateTimeInterface $createdAt;
5036

5137
private \DateTimeInterface $modifiedAt;
5238

53-
public function __construct( string $addressType, string $externalIdType, int $externalId, AddressChangeId $identifier,
54-
?Address $address = null, ?\DateTime $createdAt = null ) {
55-
$this->addressType = $addressType;
56-
$this->identifier = $identifier;
39+
public function __construct(
40+
private readonly AddressType $addressType,
41+
private readonly string $externalIdType,
42+
private readonly int $externalId,
43+
private AddressChangeId $identifier,
44+
private ?Address $address = null,
45+
?\DateTime $createdAt = null
46+
) {
5747
$this->previousIdentifier = $identifier;
58-
$this->address = $address;
59-
if ( $addressType !== self::ADDRESS_TYPE_PERSON && $addressType !== self::ADDRESS_TYPE_COMPANY ) {
60-
throw new \InvalidArgumentException( 'Invalid address type' );
61-
}
6248
if ( $externalIdType !== self::EXTERNAL_ID_TYPE_DONATION && $externalIdType !== self::EXTERNAL_ID_TYPE_MEMBERSHIP ) {
6349
throw new \InvalidArgumentException( 'Invalid external reference type' );
6450
}
6551
$this->exportDate = null;
6652
$this->createdAt = $createdAt ?? new \DateTime();
6753
$this->modifiedAt = clone $this->createdAt;
6854
$this->donationReceipt = true;
69-
$this->externalId = $externalId;
70-
$this->externalIdType = $externalIdType;
7155
}
7256

7357
public function performAddressChange( Address $address, AddressChangeId $newIdentifier ): void {
@@ -96,11 +80,11 @@ public function getAddress(): ?Address {
9680
}
9781

9882
public function isPersonalAddress(): bool {
99-
return $this->addressType === self::ADDRESS_TYPE_PERSON;
83+
return $this->addressType === AddressType::Person;
10084
}
10185

10286
public function isCompanyAddress(): bool {
103-
return $this->addressType === self::ADDRESS_TYPE_COMPANY;
87+
return $this->addressType === AddressType::Company;
10488
}
10589

10690
public function isOptedIntoDonationReceipt(): bool {

src/Domain/Model/AddressChangeBuilder.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class AddressChangeBuilder {
1010

1111
private static ?UuidGenerator $uuidGenerator = null;
1212

13-
private ?string $addressType;
13+
private ?AddressType $addressType;
1414
private ?string $referenceType;
1515
private ?int $referenceId;
1616
private AddressChangeId $identifier;
@@ -34,18 +34,14 @@ public static function create( ?AddressChangeId $identifier = null, ?Address $ad
3434
}
3535

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

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

44-
/**
45-
* @param AddressChange::ADDRESS_TYPE_PERSON|AddressChange::ADDRESS_TYPE_COMPANY $addressType
46-
* @return $this
47-
*/
48-
public function setAddressType( string $addressType ): self {
44+
public function setAddressType( AddressType $addressType ): self {
4945
if ( $this->addressType !== null ) {
5046
throw new \RuntimeException( 'You can only specify address type once' );
5147
}

src/Domain/Model/AddressType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
declare( strict_types=1 );
3+
4+
namespace WMDE\Fundraising\AddressChangeContext\Domain\Model;
5+
6+
enum AddressType {
7+
case Person;
8+
case Company;
9+
}

src/ScalarTypeConverter.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
declare( strict_types=1 );
3+
4+
namespace WMDE\Fundraising\AddressChangeContext;
5+
6+
/**
7+
* This class converts "mixed" values from libraries like Doctrine or Symfony into scalar values, without tripping up
8+
* PHPStan, which disallows calling "strval" and "intval" on variables with "mixed" type, because calling them
9+
* with objects or arrays will generate a warning.
10+
*
11+
* DO NOT USE THIS IN LOOPS! (E.g. iterating over database results).
12+
* The constant type checking will slow down the application, use PHPStan-specific comments to ignore this error instead:
13+
* https://phpstan.org/user-guide/ignoring-errors
14+
*
15+
* Hopefully, in the future libraries will return fewer "mixed" types. Please check from time to time if this class is still needed.
16+
* Check the usage of the class methods to detect libraries that return mixed.
17+
*/
18+
class ScalarTypeConverter {
19+
public static function toInt( mixed $value ): int {
20+
return intval( self::assertScalarType( $value ) );
21+
}
22+
23+
public static function toString( mixed $value ): string {
24+
return strval( self::assertScalarType( $value ) );
25+
}
26+
27+
private static function assertScalarType( mixed $value ): int|string|bool|float {
28+
if ( is_scalar( $value ) ) {
29+
return $value;
30+
}
31+
throw new \InvalidArgumentException( "Given value is not a scalar type" );
32+
}
33+
}

src/UseCases/ChangeAddress/ChangeAddressRequest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace WMDE\Fundraising\AddressChangeContext\UseCases\ChangeAddress;
66

77
use WMDE\FreezableValueObject\FreezableValueObject;
8-
use WMDE\Fundraising\AddressChangeContext\Domain\Model\AddressChange;
8+
use WMDE\Fundraising\AddressChangeContext\Domain\Model\AddressType;
99

1010
class ChangeAddressRequest {
1111
use FreezableValueObject;
@@ -28,7 +28,7 @@ class ChangeAddressRequest {
2828

2929
private string $country;
3030

31-
private string $addressType;
31+
private AddressType $addressType;
3232

3333
private string $identifier;
3434

@@ -126,19 +126,19 @@ public function setCountry( string $country ): self {
126126
return $this;
127127
}
128128

129-
public function getAddressType(): string {
129+
public function getAddressType(): AddressType {
130130
return $this->addressType;
131131
}
132132

133133
public function isPersonal(): bool {
134-
return $this->addressType === AddressChange::ADDRESS_TYPE_PERSON;
134+
return $this->addressType === AddressType::Person;
135135
}
136136

137137
public function isCompany(): bool {
138-
return $this->addressType === AddressChange::ADDRESS_TYPE_COMPANY;
138+
return $this->addressType === AddressType::Company;
139139
}
140140

141-
public function setAddressType( string $addressType ): self {
141+
public function setAddressType( AddressType $addressType ): self {
142142
$this->assertIsWritable();
143143
$this->addressType = $addressType;
144144
return $this;

0 commit comments

Comments
 (0)