Skip to content
31 changes: 4 additions & 27 deletions src/product/TariffTypeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@

namespace hiqdev\php\billing\product;

use Google\Service\TPU;
use hiqdev\php\billing\product\behavior\BehaviorCollectionInterface;
use hiqdev\php\billing\product\behavior\BehaviorInterface;
use hiqdev\php\billing\product\behavior\BehaviorTariffTypeCollection;
use hiqdev\php\billing\product\behavior\TariffTypeBehaviorRegistry;
use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface;
use hiqdev\php\billing\product\Exception\ProductNotDefinedException;
use hiqdev\php\billing\product\price\PriceTypeDefinition;
use hiqdev\php\billing\product\price\PriceTypeDefinitionCollection;
use hiqdev\php\billing\product\price\PriceTypeDefinitionCollectionInterface;
use hiqdev\php\billing\product\price\PriceTypeDefinitionFactory;
use hiqdev\php\billing\product\price\PriceTypeDefinitionInterface;
use hiqdev\php\billing\product\trait\HasLock;
use LogicException;

/**
* @template TPriceTypeDefinitionCollection of PriceTypeDefinitionCollection
Expand Down Expand Up @@ -89,24 +84,6 @@ public function withPrices()
return $this->prices;
}

public function findPricesByTypeName(string $typeName): ?array
{
$prices = null;
$this->ensureNotLocked();

foreach ($this->prices as $price) {
if ($this->matchesPriceType($price, $typeName)) {
$prices[] = $price;
}
}
return $prices;
}

private function matchesPriceType(PriceTypeDefinitionInterface $price, string $typeName): bool
{
return str_ends_with($price->type()->getName(), ",$typeName");
}

/**
* @return BehaviorCollectionInterface<TariffTypeDefinition>
* @psalm-suppress ImplementedReturnTypeMismatch
Expand All @@ -117,15 +94,15 @@ public function withBehaviors()
{
$this->ensureNotLocked();

return $this->tariffTypeBehaviorRegistry->getBehaviors();
return $this->tariffTypeBehaviorRegistry->withBehaviors();
}

public function hasBehavior(string $behaviorClassName): bool
{
return $this->tariffTypeBehaviorRegistry->hasBehavior($behaviorClassName);
}

public function findBehaviorByClass(string $class): ?BehaviorInterface
public function findBehaviorByClass(string $class)
{
return $this->tariffTypeBehaviorRegistry->findBehaviorByClass($class);
}
Expand All @@ -136,7 +113,7 @@ public function end(): TariffTypeDefinitionInterface

// Validate prices configuration is complete
if ($this->prices->count() === 0) {
throw new \LogicException('At least one price type must be defined');
throw new LogicException('At least one price type must be defined');
}

return $this;
Expand Down
2 changes: 0 additions & 2 deletions src/product/TariffTypeDefinitionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,4 @@ public function setPricesSuggester(string $suggesterClass): static;
public function withPrices();

public function end();

public function findPricesByTypeName(string $typeName): ?array;
}
41 changes: 41 additions & 0 deletions src/product/behavior/BehaviorRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace hiqdev\php\billing\product\behavior;

use hiqdev\php\billing\product\trait\HasLockInterface;

/**
* @psalm-suppress MissingTemplateParam
* @psalm-suppress InvalidTemplateParam
*/
abstract class BehaviorRegistry implements HasLockInterface, HasBehaviorsInterface
{
public function hasBehavior(string $behaviorClassName): bool
{
foreach ($this->withBehaviors() as $behavior) {
if ($behavior instanceof $behaviorClassName) {
return true;
}
}

return false;
}

public function findBehaviorByClass(string $class)
{
foreach ($this->withBehaviors() as $behavior) {
if ($behavior instanceof $class) {
return $behavior;
}
}

return null;
}

public function lock(): void
{
$this->withBehaviors()->lock();
}
}
8 changes: 6 additions & 2 deletions src/product/behavior/HasBehaviorsInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace hiqdev\php\billing\product\behavior;

use hiqdev\php\billing\product\invoice\RepresentationCollection;
use hiqdev\php\billing\product\price\PriceTypeDefinitionInterface;
use hiqdev\php\billing\product\TariffTypeDefinitionInterface;

Expand All @@ -19,5 +18,10 @@ public function withBehaviors();

public function hasBehavior(string $behaviorClassName): bool;

public function findBehaviorByClass(string $class): ?BehaviorInterface;
/**
* @template TBehavior of object
* @param class-string<TBehavior> $class
* @return TBehavior|null
*/
public function findBehaviorByClass(string $class);
}
40 changes: 40 additions & 0 deletions src/product/behavior/PriceTypeBehaviorRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace hiqdev\php\billing\product\behavior;

use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface;
use hiqdev\php\billing\product\price\PriceTypeDefinitionInterface;

/**
* @template-covariant T of PriceTypeDefinitionInterface
*/
final class PriceTypeBehaviorRegistry extends BehaviorRegistry
{
/**
* @var BehaviorPriceTypeDefinitionCollection<T>
*/
private BehaviorPriceTypeDefinitionCollection $behaviorCollection;

/**
* @psalm-param T $priceTypeDefinition
*/
public function __construct(PriceTypeDefinitionInterface $priceTypeDefinition, TariffTypeInterface $tariffType)
{
$this->behaviorCollection = new BehaviorPriceTypeDefinitionCollection($priceTypeDefinition, $tariffType);
}

/**
* @return BehaviorPriceTypeDefinitionCollection<T>
*/
public function withBehaviors(): BehaviorPriceTypeDefinitionCollection
{
return $this->behaviorCollection;
}

protected function getBehaviorCollection(): BehaviorCollectionInterface
{
return $this->behaviorCollection;
}
}
30 changes: 4 additions & 26 deletions src/product/behavior/TariffTypeBehaviorRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*
* @template-covariant T of TariffTypeDefinitionInterface
*/
final class TariffTypeBehaviorRegistry implements HasLockInterface
final class TariffTypeBehaviorRegistry extends BehaviorRegistry
{
/**
* @var BehaviorTariffTypeCollection<T>
Expand All @@ -44,35 +44,13 @@ public function __construct(TariffTypeDefinitionInterface $tariffTypeDefinition,
/**
* @return BehaviorTariffTypeCollection<T>
*/
public function getBehaviors(): BehaviorTariffTypeCollection
public function withBehaviors(): BehaviorTariffTypeCollection
{
return $this->behaviorCollection;
}
Comment on lines +47 to 50
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

withBehaviors() rename: verify all call sites migrated from getBehaviors()

Rename looks good. Please ensure no usages of getBehaviors() remain.

Run this script to find leftovers and confirm new usages:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for old API getBehaviors("
rg -n --hidden --no-ignore -S '\bgetBehaviors\(' || true

echo
echo "Searching for new API withBehaviors("
rg -n --hidden --no-ignore -S '\bwithBehaviors\(' || true

Length of output: 2746


withBehaviors() rename verification

All call-sites of the old getBehaviors() API have been updated to withBehaviors(). The only remaining reference is in the docblock of the registry class:

• src/product/behavior/TariffTypeBehaviorRegistry.php:19
Change “Provides access to the behavior collection via getBehaviors().” to “via withBehaviors().”

🤖 Prompt for AI Agents
In src/product/behavior/TariffTypeBehaviorRegistry.php around lines 19, update
the class docblock to replace the phrase "Provides access to the behavior
collection via getBehaviors()." with "Provides access to the behavior collection
via withBehaviors()." to reflect the method rename accurately.


public function hasBehavior(string $behaviorClassName): bool
protected function getBehaviorCollection(): BehaviorCollectionInterface
{
foreach ($this->behaviorCollection as $behavior) {
if ($behavior instanceof $behaviorClassName) {
return true;
}
}

return false;
}

public function findBehaviorByClass(string $class): ?BehaviorInterface
{
foreach ($this->getBehaviors() as $behavior) {
if ($behavior instanceof $class) {
return $behavior;
}
}

return null;
}

public function lock(): void
{
$this->behaviorCollection->lock();
return $this->behaviorCollection;
}
}
37 changes: 11 additions & 26 deletions src/product/price/PriceTypeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
namespace hiqdev\php\billing\product\price;

use hiqdev\php\billing\product\AggregateInterface;
use hiqdev\php\billing\product\behavior\BehaviorCollectionInterface;
use hiqdev\php\billing\product\behavior\BehaviorInterface;
use hiqdev\php\billing\product\behavior\HasBehaviorsInterface;
use hiqdev\php\billing\product\behavior\PriceTypeBehaviorRegistry;
use hiqdev\php\billing\product\Exception\AggregateNotDefinedException;
use hiqdev\php\billing\product\behavior\BehaviorPriceTypeDefinitionCollection;
use hiqdev\php\billing\product\invoice\RepresentationCollection;
Expand All @@ -22,6 +21,7 @@
use hiqdev\php\billing\product\TariffTypeDefinitionInterface;
use hiqdev\php\billing\product\trait\HasLock;
use hiqdev\php\billing\type\TypeInterface;
use function class_exists;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate formatter contract, not just existence

class_exists prevents fatals, but validating the class implements QuantityFormatterInterface will fail earlier and with a clearer message.

-        if (!class_exists($formatterClass)) {
+        if (!class_exists($formatterClass)) {
             throw new InvalidQuantityFormatterException("Formatter class $formatterClass does not exist");
         }
+        if (!is_subclass_of($formatterClass, QuantityFormatterInterface::class)) {
+            throw new InvalidQuantityFormatterException(sprintf(
+                'Formatter class %s must implement %s',
+                $formatterClass,
+                QuantityFormatterInterface::class
+            ));
+        }

Also applies to: 107-112

🤖 Prompt for AI Agents
In src/product/price/PriceTypeDefinition.php at line 24 and also lines 107 to
112, the current code uses class_exists to check for the formatter class
existence, which prevents fatal errors but does not ensure the class implements
QuantityFormatterInterface. Update the validation to check that the class both
exists and implements QuantityFormatterInterface, throwing an early and clear
exception if it does not. This will improve error clarity and contract
enforcement.


/**
* @template TParentCollection
Expand All @@ -46,16 +46,13 @@ class PriceTypeDefinition implements PriceTypeDefinitionInterface
*/
private RepresentationCollection $representationCollection;

/**
* @var BehaviorPriceTypeDefinitionCollection<PriceTypeDefinition>
*/
private BehaviorPriceTypeDefinitionCollection $behaviorCollection;

private ?AggregateInterface $aggregate = null;

/** @psalm-var TParentCollection */
private readonly PriceTypeDefinitionCollectionInterface $parent;

private readonly PriceTypeBehaviorRegistry $behaviorRegistry;

/**
* @param TParentCollection $parent
*/
Expand All @@ -66,7 +63,7 @@ public function __construct(
) {
$this->parent = $parent;
$this->representationCollection = new RepresentationCollection($this);
$this->behaviorCollection = new BehaviorPriceTypeDefinitionCollection($this, $tariffType);
$this->behaviorRegistry = new PriceTypeBehaviorRegistry($this, $tariffType);

$this->init();
}
Expand Down Expand Up @@ -107,7 +104,7 @@ public function quantityFormatter(string $formatterClass, $fractionUnit = null):
{
$this->ensureNotLocked();

if (!\class_exists($formatterClass)) {
if (!class_exists($formatterClass)) {
throw new InvalidQuantityFormatterException("Formatter class $formatterClass does not exist");
}

Expand Down Expand Up @@ -181,29 +178,17 @@ public function withBehaviors()
{
$this->ensureNotLocked();

return $this->behaviorCollection;
return $this->behaviorRegistry->withBehaviors();
}

public function hasBehavior(string $behaviorClassName): bool
{
foreach ($this->behaviorCollection as $behavior) {
if ($behavior instanceof $behaviorClassName) {
return true;
}
}

return false;
return $this->behaviorRegistry->hasBehavior($behaviorClassName);
}

public function findBehaviorByClass(string $class): ?BehaviorInterface
public function findBehaviorByClass(string $class)
{
foreach ($this->behaviorCollection as $behavior) {
if ($behavior instanceof $class) {
return $behavior;
}
}

return null;
return $this->behaviorRegistry->findBehaviorByClass($class);
}

/**
Expand Down Expand Up @@ -242,7 +227,7 @@ public function getQuantityFormatterDefinition(): ?QuantityFormatterDefinition
protected function afterLock(): void
{
$this->representationCollection->lock();
$this->behaviorCollection->lock();
$this->behaviorRegistry->lock();
}

public function getTariffTypeDefinition(): TariffTypeDefinitionInterface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ protected function setUp(): void

public function testWithBehaviorsReturnsBehaviorCollection(): void
{
$this->assertInstanceOf(BehaviorTariffTypeCollection::class, $this->manager->getBehaviors());
$this->assertInstanceOf(BehaviorTariffTypeCollection::class, $this->manager->withBehaviors());
}

public function testHasBehaviorReturnsFalseWhenBehaviorNotPresent(): void
Expand All @@ -32,7 +32,7 @@ public function testHasBehaviorReturnsFalseWhenBehaviorNotPresent(): void
public function testHasBehaviorReturnsTrueWhenBehaviorPresent(): void
{
$behavior = $this->createMock(TestBehavior::class);
$behaviorCollection = $this->manager->getBehaviors();
$behaviorCollection = $this->manager->withBehaviors();
$behaviorCollection->attach($behavior);

$this->assertTrue($this->manager->hasBehavior(TestBehavior::class));
Expand Down