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

Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a9e3868
HP-2419 extended functional of BillingRegistry for easier use
VadymHrechukha May 6, 2025
055c135
HP-2419 extended functional of BillingRegistry for easier use
VadymHrechukha May 6, 2025
02f0958
HP-2419 tiny
VadymHrechukha May 7, 2025
d7937d3
HP-2419 tiny
VadymHrechukha May 7, 2025
8fad103
HP-2419 tiny
VadymHrechukha May 7, 2025
d993cd0
HP-2419 tiny
VadymHrechukha May 7, 2025
405f064
HP-2419 tiny
VadymHrechukha May 7, 2025
d7b0e2a
HP-2419 tiny
VadymHrechukha May 7, 2025
f48a47a
HP-2419 tiny
VadymHrechukha May 7, 2025
3cf9e05
HP-2419 fixing Psalm error
VadymHrechukha May 7, 2025
f062de0
HP-2419 removed dependency between php-billing and BillingRegistry pa…
VadymHrechukha May 7, 2025
be55965
HP-2419 created TrafCollectorInterface
VadymHrechukha May 7, 2025
0856451
HP-2419 fixing PHPUnit tests
VadymHrechukha May 7, 2025
167436f
HP-2419 fixing PHPUnit tests
VadymHrechukha May 7, 2025
27bec10
HP-2419 implemented Service for BillingRegistry and allow client code…
VadymHrechukha May 7, 2025
8eba047
HP-2419 Created PHPUnit test for BillingRegistryService
VadymHrechukha May 7, 2025
254739f
HP-2419 fixing scrutinizer
VadymHrechukha May 14, 2025
47af1db
HP-2419 fixing scrutinizer
VadymHrechukha May 14, 2025
964147f
HP-2419 fixing scrutinizer
VadymHrechukha May 14, 2025
b92bf22
HP-2419 fixing scrutinizer
VadymHrechukha May 14, 2025
e39a529
HP-2419 fixing scrutinizer
VadymHrechukha May 14, 2025
a699807
HP-2419 fixing scrutinizer
VadymHrechukha May 14, 2025
e6ad068
HP-2419 fixing scrutinizer
VadymHrechukha May 14, 2025
dea56cf
HP-2419 exclude vendor and tests from scrutinizer
VadymHrechukha May 14, 2025
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
Loading