Skip to content

HP-2419 extended functional of BillingRegistry for easier use #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions src/product/Application/BillingRegistryService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php declare(strict_types=1);

namespace hiqdev\php\billing\product\Application;

use hiqdev\php\billing\product\AggregateInterface;
use hiqdev\php\billing\product\behavior\BehaviorInterface;
use hiqdev\php\billing\product\behavior\BehaviorNotFoundException;
use hiqdev\php\billing\product\behavior\InvalidBehaviorException;
use hiqdev\php\billing\product\BillingRegistryInterface;
use hiqdev\php\billing\product\Exception\AggregateNotFoundException;
use hiqdev\php\billing\product\Exception\TariffTypeDefinitionNotFoundException;
use hiqdev\php\billing\product\invoice\InvalidRepresentationException;
use hiqdev\php\billing\product\invoice\RepresentationInterface;
use hiqdev\php\billing\product\price\PriceTypeDefinitionInterface;
use hiqdev\php\billing\product\quantity\FractionQuantityData;
use hiqdev\php\billing\product\quantity\QuantityFormatterInterface;
use hiqdev\php\billing\product\quantity\QuantityFormatterNotFoundException;
use hiqdev\php\billing\product\TariffTypeDefinitionInterface;
use hiqdev\php\billing\type\Type;
use hiqdev\php\billing\type\TypeInterface;

final class BillingRegistryService implements BillingRegistryServiceInterface
{
public function __construct(private readonly BillingRegistryInterface $registry)
{
}

public function getRepresentationsByType(string $representationClass): array
{
if (!class_exists($representationClass)) {
throw new InvalidRepresentationException("Class '$representationClass' does not exist");
}

if (!is_subclass_of($representationClass, RepresentationInterface::class)) {
throw new InvalidBehaviorException(
sprintf('Representation class "%s" does not implement RepresentationInterface', $representationClass)
);
}
Comment on lines +30 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect exception type for representation-interface validation

When the provided class is not a RepresentationInterface, InvalidBehaviorException is thrown.
That is confusing, because the error is about representation misuse, not behavior misuse, and clients relying on the more specific InvalidRepresentationException will silently miss it.

-            throw new InvalidBehaviorException(
-                sprintf('Representation class "%s" does not implement RepresentationInterface', $representationClass)
-            );
+            throw new InvalidRepresentationException(
+                sprintf('Representation class "%s" does not implement %s', $representationClass, RepresentationInterface::class)
+            );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!class_exists($representationClass)) {
throw new InvalidRepresentationException("Class '$representationClass' does not exist");
}
if (!is_subclass_of($representationClass, RepresentationInterface::class)) {
throw new InvalidBehaviorException(
sprintf('Representation class "%s" does not implement RepresentationInterface', $representationClass)
);
}
if (!class_exists($representationClass)) {
throw new InvalidRepresentationException("Class '$representationClass' does not exist");
}
if (!is_subclass_of($representationClass, RepresentationInterface::class)) {
- throw new InvalidBehaviorException(
- sprintf('Representation class "%s" does not implement RepresentationInterface', $representationClass)
- );
+ throw new InvalidRepresentationException(
+ sprintf('Representation class "%s" does not implement %s', $representationClass, RepresentationInterface::class)
+ );
}


$representations = [];
foreach ($this->registry->priceTypes() as $priceTypeDefinition) {
foreach ($priceTypeDefinition->documentRepresentation() as $representation) {
if ($representation instanceof $representationClass) {
$representations[] = $representation;
}
}
}

return $representations;
}

public function createQuantityFormatter(string $type, FractionQuantityData $data): QuantityFormatterInterface {
$type = $this->convertStringTypeToType($type);

foreach ($this->registry->priceTypes() as $priceTypeDefinition) {
if ($priceTypeDefinition->hasType($type)) {
return $priceTypeDefinition->createQuantityFormatter($data);
}
}

throw new QuantityFormatterNotFoundException('Quantity formatter not found');
}

private function convertStringTypeToType(string $type): TypeInterface
{
return Type::anyId($type);
}

public function getBehavior(string $type, string $behaviorClassWrapper): BehaviorInterface
{
if (!class_exists($behaviorClassWrapper)) {
throw new InvalidBehaviorException(
sprintf('Behavior class "%s" does not exist', $behaviorClassWrapper)
);
}

if (!is_subclass_of($behaviorClassWrapper, BehaviorInterface::class)) {
throw new InvalidBehaviorException(
sprintf('Behavior class "%s" does not implement BehaviorInterface', $behaviorClassWrapper)
);
}
Comment on lines +71 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validation logic duplicated & missing in sibling methods

Excellent job validating $behaviorClassWrapper here.
However, the same guard clauses are absent in getBehaviors() and findPriceTypeDefinitionsByBehavior().
Calling those methods with an unknown class string will raise a fatal Error: Class "<name>" not found.

Extract the validation into a small private helper and reuse it.


$billingType = $this->convertStringTypeToType($type);

foreach ($this->registry->priceTypes() as $priceTypeDefinition) {
if ($priceTypeDefinition->hasType($billingType)) {
$behavior = $this->findBehaviorInPriceType($priceTypeDefinition, $behaviorClassWrapper);

if ($behavior) {
return $behavior;
}
}
}

throw new BehaviorNotFoundException(
sprintf('Behavior of class "%s" not found for type "%s"', $behaviorClassWrapper, $type),
);
}

private function findBehaviorInPriceType(
PriceTypeDefinitionInterface $priceTypeDefinition,
string $behaviorClassWrapper
): ?BehaviorInterface {
foreach ($priceTypeDefinition->withBehaviors() as $behavior) {
if ($behavior instanceof $behaviorClassWrapper) {
return $behavior;
}
}

return null;
}

public function getBehaviors(string $behaviorClassWrapper): \Generator
{
foreach ($this->registry->getTariffTypeDefinitions() as $tariffTypeDefinition) {
foreach ($tariffTypeDefinition->withBehaviors() as $behavior) {
if ($behavior instanceof $behaviorClassWrapper) {
yield $behavior;
}
}
}

foreach ($this->registry->priceTypes() as $priceTypeDefinition) {
foreach ($priceTypeDefinition->withBehaviors() as $behavior) {
if ($behavior instanceof $behaviorClassWrapper) {
yield $behavior;
}
}
}
}
Comment on lines +113 to +130
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add class existence / interface checks to prevent fatal errors

See previous comment. Before the two nested loops, add:

if (!class_exists($behaviorClassWrapper) ||
    !is_subclass_of($behaviorClassWrapper, BehaviorInterface::class)) {
    throw new InvalidBehaviorException(
        sprintf('Behavior class "%s" is invalid or does not implement %s',
            $behaviorClassWrapper,
            BehaviorInterface::class
        )
    );
}


public function getAggregate(string $type): AggregateInterface
{
$type = $this->convertStringTypeToType($type);

foreach ($this->registry->priceTypes() as $priceTypeDefinition) {
if ($priceTypeDefinition->hasType($type)) {
return $priceTypeDefinition->getAggregate();
}
}

throw new AggregateNotFoundException('Aggregate was not found');
}

public function findTariffTypeDefinitionByBehavior(BehaviorInterface $behavior): TariffTypeDefinitionInterface
{
$tariffType = $behavior->getTariffType();

foreach ($this->registry->getTariffTypeDefinitions() as $tariffTypeDefinition) {
if ($tariffTypeDefinition->belongToTariffType($tariffType)) {
return $tariffTypeDefinition;
}
}

throw new TariffTypeDefinitionNotFoundException('Tariff type definition was not found');
}

public function findPriceTypeDefinitionsByBehavior(string $behaviorClassWrapper): \Generator
{
foreach ($this->registry->priceTypes() as $priceTypeDefinition) {
if ($priceTypeDefinition->hasBehavior($behaviorClassWrapper)) {
yield $priceTypeDefinition;
}
}
}
Comment on lines +158 to +165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same validation gap as in getBehaviors()

Add the class_exists/is_subclass_of guard here as well to avoid runtime fatals.

}
54 changes: 54 additions & 0 deletions src/product/Application/BillingRegistryServiceInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types=1);

namespace hiqdev\php\billing\product\Application;

use Generator;
use hiqdev\php\billing\product\AggregateInterface;
use hiqdev\php\billing\product\behavior\BehaviorInterface;
use hiqdev\php\billing\product\behavior\BehaviorNotFoundException;
use hiqdev\php\billing\product\behavior\InvalidBehaviorException;
use hiqdev\php\billing\product\invoice\RepresentationInterface;
use hiqdev\php\billing\product\price\PriceTypeDefinitionInterface;
use hiqdev\php\billing\product\quantity\FractionQuantityData;
use hiqdev\php\billing\product\quantity\QuantityFormatterInterface;
use hiqdev\php\billing\product\TariffTypeDefinitionInterface;

interface BillingRegistryServiceInterface
{
/**
* @param string $representationClass
* @return RepresentationInterface[]
*/
public function getRepresentationsByType(string $representationClass): array;

public function createQuantityFormatter(string $type, FractionQuantityData $data): QuantityFormatterInterface;

/**
* @param string $type - full type like 'overuse,lb_capacity_unit'
* @param string $behaviorClassWrapper
* @return BehaviorInterface
* @throws BehaviorNotFoundException
* @throws InvalidBehaviorException
*/
public function getBehavior(string $type, string $behaviorClassWrapper): BehaviorInterface;

/**
* Find all behaviors attached to any TariffType or PriceType by specified Behavior class.
*
* @param string $behaviorClassWrapper
* @return Generator<BehaviorInterface>
*/
public function getBehaviors(string $behaviorClassWrapper): Generator;

public function getAggregate(string $type): AggregateInterface;

public function findTariffTypeDefinitionByBehavior(BehaviorInterface $behavior): TariffTypeDefinitionInterface;

/**
* Find all PriceTypeDefinition in registry by specified Behavior class.
*
* @param string $behaviorClassWrapper
* @return Generator<PriceTypeDefinitionInterface>
*/
public function findPriceTypeDefinitionsByBehavior(string $behaviorClassWrapper): Generator;
}
147 changes: 6 additions & 141 deletions src/product/BillingRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,14 @@

namespace hiqdev\php\billing\product;

use hiqdev\php\billing\product\behavior\InvalidBehaviorException;
use hiqdev\php\billing\product\Exception\AggregateNotFoundException;
use hiqdev\php\billing\product\invoice\InvalidRepresentationException;
use hiqdev\php\billing\product\invoice\RepresentationInterface;
use hiqdev\php\billing\product\price\PriceTypeDefinition;
use hiqdev\php\billing\product\quantity\QuantityFormatterInterface;
use hiqdev\php\billing\product\quantity\QuantityFormatterNotFoundException;
use hiqdev\php\billing\product\quantity\FractionQuantityData;
use hiqdev\php\billing\product\behavior\BehaviorInterface;
use hiqdev\php\billing\product\behavior\BehaviorNotFoundException;
use hiqdev\php\billing\product\trait\HasLock;
use hiqdev\php\billing\type\Type;
use hiqdev\php\billing\type\TypeInterface;

class BillingRegistry implements BillingRegistryInterface
{
use HasLock;

/** @var TariffTypeDefinitionInterface[] */
private array $tariffTypeDefinitions = [];
private bool $locked = false;

public function addTariffType(TariffTypeDefinitionInterface $tariffTypeDefinition): void
{
Expand All @@ -31,140 +18,18 @@ public function addTariffType(TariffTypeDefinitionInterface $tariffTypeDefinitio
$this->tariffTypeDefinitions[] = $tariffTypeDefinition;
}

public function priceTypes(): \Generator
{
foreach ($this->tariffTypeDefinitions as $tariffTypeDefinition) {
foreach ($tariffTypeDefinition->withPrices() as $priceTypeDefinition) {
yield $priceTypeDefinition;
}
}
}

public function getRepresentationsByType(string $representationClass): array
{
if (!class_exists($representationClass)) {
throw new InvalidRepresentationException("Class '$representationClass' does not exist");
}

if (!is_subclass_of($representationClass, RepresentationInterface::class)) {
throw new InvalidBehaviorException(
sprintf('Representation class "%s" does not implement RepresentationInterface', $representationClass)
);
}

$representations = [];
foreach ($this->priceTypes() as $priceTypeDefinition) {
foreach ($priceTypeDefinition->documentRepresentation() as $representation) {
if ($representation instanceof $representationClass) {
$representations[] = $representation;
}
}
}

return $representations;
}

public function createQuantityFormatter(
string $type,
FractionQuantityData $data,
): QuantityFormatterInterface {
$type = $this->convertStringTypeToType($type);

foreach ($this->priceTypes() as $priceTypeDefinition) {
if ($priceTypeDefinition->hasType($type)) {
return $priceTypeDefinition->createQuantityFormatter($data);
}
}

throw new QuantityFormatterNotFoundException('Quantity formatter not found');
}

private function convertStringTypeToType(string $type): TypeInterface
{
return Type::anyId($type);
}

public function getBehavior(string $type, string $behaviorClassWrapper): BehaviorInterface
{
if (!class_exists($behaviorClassWrapper)) {
throw new InvalidBehaviorException(
sprintf('Behavior class "%s" does not exist', $behaviorClassWrapper)
);
}

if (!is_subclass_of($behaviorClassWrapper, BehaviorInterface::class)) {
throw new InvalidBehaviorException(
sprintf('Behavior class "%s" does not implement BehaviorInterface', $behaviorClassWrapper)
);
}

$billingType = $this->convertStringTypeToType($type);

foreach ($this->priceTypes() as $priceTypeDefinition) {
if ($priceTypeDefinition->hasType($billingType)) {
$behavior = $this->findBehaviorInPriceType($priceTypeDefinition, $behaviorClassWrapper);

if ($behavior) {
return $behavior;
}
}
}

throw new BehaviorNotFoundException(
sprintf('Behavior of class "%s" not found for type "%s"', $behaviorClassWrapper, $type),
);
}

private function findBehaviorInPriceType(
PriceTypeDefinition $priceTypeDefinition,
string $behaviorClassWrapper
): ?BehaviorInterface {
foreach ($priceTypeDefinition->withBehaviors() as $behavior) {
if ($behavior instanceof $behaviorClassWrapper) {
return $behavior;
}
}

return null;
}

public function getBehaviors(string $behaviorClassWrapper): \Generator
public function getTariffTypeDefinitions(): array
{
foreach ($this->tariffTypeDefinitions as $tariffTypeDefinition) {
foreach ($tariffTypeDefinition->withBehaviors() as $behavior) {
if ($behavior instanceof $behaviorClassWrapper) {
yield $behavior;
}
}
}

foreach ($this->priceTypes() as $priceTypeDefinition) {
foreach ($priceTypeDefinition->withBehaviors() as $behavior) {
if ($behavior instanceof $behaviorClassWrapper) {
yield $behavior;
}
}
}
return $this->tariffTypeDefinitions;
}

public function getAggregate(string $type): AggregateInterface
public function priceTypes(): \Generator
{
$type = $this->convertStringTypeToType($type);

foreach ($this->priceTypes() as $priceTypeDefinition) {
if ($priceTypeDefinition->hasType($type)) {
return $priceTypeDefinition->getAggregate();
foreach ($this->getTariffTypeDefinitions() as $tariffTypeDefinition) {
foreach ($tariffTypeDefinition->withPrices() as $priceTypeDefinition) {
yield $priceTypeDefinition;
}
}

throw new AggregateNotFoundException('Aggregate was not found');
}

public function getTariffTypeDefinitions(): \Generator
{
foreach ($this->tariffTypeDefinitions as $tariffTypeDefinition) {
yield $tariffTypeDefinition;
}
}

protected function afterLock(): void
Expand Down
Loading