Repository: pheature-flags/pheature-flags (php)

Issue:
[Toggle Model] Create Strategy factory for our existing Toggle strategy types 
**Epic:** https://github.com/pheature-flags/pheature-flags/issues/2 Toggle Model
**Package:** https://github.com/pheature-flags/toggle-model 

**Description:**

Currently, we only have two types of ToggleStrategy implemented, the EnableByMatchingSegment class and EnableByMatchingIdentityId.

We need to create a factory that implements the ToggleStrategyFactory. This factory should be able to create instances of both Toggle Strategy types.

Pull request description:
Issue#114 create factory for existing strategy types
Create model StrategyFactory, fixes EnableByMatchingIdentityId, and adds tests. Closes #114. With this code merged, and finishing the epic #122,  we can now start focusing on docs and framework integrations to work as monolith:

We already have:

- [x] **Toggle Core, Read and Write models defined**
- [x] **Toggle CRUD and HTTP API**
- [ ] **Toggle PSR-11 Factories**



Patch:
diff --git a/packages/toggle-core/src/Read/Segments.php b/packages/toggle-core/src/Read/Segments.php
index 99c9705..f24d7c7 100644
--- a/packages/toggle-core/src/Read/Segments.php
+++ b/packages/toggle-core/src/Read/Segments.php
@@ -10,15 +10,13 @@ use function array_values;
 final class Segments
 {
     /**
-     * @var array<string, Segment>
+     * @var Segment[]
      */
     private array $segments = [];
 
     public function __construct(Segment ...$segments)
     {
-        foreach ($segments as $segment) {
-            $this->segments[$segment->id()] = $segment;
-        }
+        $this->segments = $segments;
     }
 
     /**
@@ -26,6 +24,6 @@ final class Segments
      */
     public function all(): array
     {
-        return array_values($this->segments);
+        return $this->segments;
     }
 }
diff --git a/packages/toggle-model/src/EnableByMatchingIdentityId.php b/packages/toggle-model/src/EnableByMatchingIdentityId.php
index 414de98..47474a4 100644
--- a/packages/toggle-model/src/EnableByMatchingIdentityId.php
+++ b/packages/toggle-model/src/EnableByMatchingIdentityId.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace Pheature\Model\Toggle;
 
+use InvalidArgumentException;
 use Pheature\Core\Toggle\Read\ConsumerIdentity;
 use Pheature\Core\Toggle\Read\Segment as ISegment;
 use Pheature\Core\Toggle\Read\Segments;
@@ -11,10 +12,19 @@ use Pheature\Core\Toggle\Read\ToggleStrategy;
 
 final class EnableByMatchingIdentityId implements ToggleStrategy
 {
+    public const NAME = 'enable_by_matching_identity_id';
     private Segments $segments;
 
     public function __construct(Segments $segments)
     {
+        foreach ($segments->all() as $segment) {
+            if (false === $segment instanceof IdentitySegment) {
+                throw new InvalidArgumentException(sprintf(
+                    'Enable by matching identity id segment must be instance of %s class.',
+                    IdentitySegment::class
+                ));
+            }
+        }
         $this->segments = $segments;
     }
 
@@ -29,13 +39,10 @@ final class EnableByMatchingIdentityId implements ToggleStrategy
         return false;
     }
 
-    /**
-     * @return array<string, string|array>
-     */
     public function toArray(): array
     {
         return [
-            'type' => 'enable_by_matching_identity_id',
+            'type' => self::NAME,
             'segments' => array_map(
                 static fn(ISegment $segment): array => $segment->toArray(),
                 $this->segments->all()
diff --git a/packages/toggle-model/src/EnableByMatchingSegment.php b/packages/toggle-model/src/EnableByMatchingSegment.php
index 6ccb50e..8343fdc 100644
--- a/packages/toggle-model/src/EnableByMatchingSegment.php
+++ b/packages/toggle-model/src/EnableByMatchingSegment.php
@@ -11,6 +11,7 @@ use Pheature\Core\Toggle\Read\ToggleStrategy;
 
 final class EnableByMatchingSegment implements ToggleStrategy
 {
+    public const NAME = 'enable_by_matching_segment';
     private Segments $segments;
 
     public function __construct(Segments $segments)
@@ -35,7 +36,7 @@ final class EnableByMatchingSegment implements ToggleStrategy
     public function toArray(): array
     {
         return [
-            'type' => 'enable_by_matching_segment',
+            'type' => self::NAME,
             'segments' => array_map(
                 static fn(ISegment $segment): array => $segment->toArray(),
                 $this->segments->all()
diff --git a/packages/toggle-model/src/IdentitySegment.php b/packages/toggle-model/src/IdentitySegment.php
new file mode 100644
index 0000000..a295d18
--- /dev/null
+++ b/packages/toggle-model/src/IdentitySegment.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pheature\Model\Toggle;
+
+use Pheature\Core\Toggle\Read\Segment;
+
+final class IdentitySegment implements Segment
+{
+    private string $id;
+    /**
+     * @var array<string, mixed>
+     */
+    private array $criteria;
+
+    /**
+     * Segment constructor.
+     *
+     * @param string               $id
+     * @param array<string, mixed> $criteria
+     */
+    public function __construct(string $id, array $criteria)
+    {
+        $this->id = $id;
+        $this->criteria = $criteria;
+    }
+
+    public function id(): string
+    {
+        return $this->id;
+    }
+
+    /**
+     * @return array<string, mixed>
+     */
+    public function criteria(): array
+    {
+        return $this->criteria;
+    }
+
+    public function match(array $payload): bool
+    {
+        if (false === array_key_exists('identity_id', $payload)) {
+            return false;
+        }
+
+        /** @var string $identityId */
+        $identityId = $payload['identity_id'];
+
+        return in_array($identityId, $this->criteria, true);
+    }
+
+    /**
+     * @return array<string, string|array>
+     */
+    public function toArray(): array
+    {
+        return [
+            'id' => $this->id,
+            'criteria' => $this->criteria,
+        ];
+    }
+
+    /**
+     * @return array<string, string|array>
+     */
+    public function jsonSerialize(): array
+    {
+        return $this->toArray();
+    }
+}
diff --git a/packages/toggle-model/src/StrategyFactory.php b/packages/toggle-model/src/StrategyFactory.php
new file mode 100644
index 0000000..0bfcf5d
--- /dev/null
+++ b/packages/toggle-model/src/StrategyFactory.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pheature\Model\Toggle;
+
+use Pheature\Core\Toggle\Exception\InvalidStrategyTypeGiven;
+use Pheature\Core\Toggle\Read\Segments;
+use Pheature\Core\Toggle\Read\ToggleStrategy;
+use Pheature\Core\Toggle\Read\ToggleStrategyFactory;
+
+final class StrategyFactory implements ToggleStrategyFactory
+{
+    public function create(string $strategyId, string $strategyType, ?Segments $segments = null): ToggleStrategy
+    {
+        $segments = $segments ?? new Segments();
+        if (EnableByMatchingSegment::NAME === $strategyType) {
+            return new EnableByMatchingSegment($segments);
+        }
+        if (EnableByMatchingIdentityId::NAME === $strategyType) {
+            return new EnableByMatchingIdentityId($segments);
+        }
+
+        throw InvalidStrategyTypeGiven::withType($strategyType);
+    }
+
+    public function types(): array
+    {
+        return [
+            EnableByMatchingSegment::NAME,
+            EnableByMatchingIdentityId::NAME,
+        ];
+    }
+}


Test patch:
diff --git a/packages/toggle-model/test/EnableByMatchingIdentityIdTest.php b/packages/toggle-model/test/EnableByMatchingIdentityIdTest.php
new file mode 100644
index 0000000..421dd43
--- /dev/null
+++ b/packages/toggle-model/test/EnableByMatchingIdentityIdTest.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pheature\Test\Model\Toggle;
+
+use InvalidArgumentException;
+use Pheature\Core\Toggle\Read\Segments;
+use Pheature\Model\Toggle\EnableByMatchingIdentityId;
+use Pheature\Model\Toggle\Identity;
+use Pheature\Model\Toggle\IdentitySegment;
+use Pheature\Model\Toggle\Segment;
+use PHPUnit\Framework\TestCase;
+
+final class EnableByMatchingIdentityIdTest extends TestCase
+{
+    public function testItShouldOnlyAcceptIdentitySegments(): void
+    {
+        $this->expectException(InvalidArgumentException::class);
+
+        $segments = new Segments(new Segment('some_segments', []));
+        new EnableByMatchingIdentityId($segments);
+    }
+
+    public function testItShouldBeSatisfiedByMatchingIdentitySegment(): void
+    {
+        $segments = new Segments(new IdentitySegment('developers', [
+            'some_id',
+        ]));
+
+        $strategy = new EnableByMatchingIdentityId($segments);
+        self::assertTrue($strategy->isSatisfiedBy(new Identity('some_id')));
+    }
+
+    public function testItShouldNotBeSatisfiedWithoutAnyIdentitySegment(): void
+    {
+        $segments = new Segments();
+
+        $strategy = new EnableByMatchingIdentityId($segments);
+        self::assertFalse($strategy->isSatisfiedBy(new Identity('some_id', [
+            'some_id',
+        ])));
+    }
+
+    public function testItShouldNotBeSatisfiedByUnMatchingIdentitySegment(): void
+    {
+        $segments = new Segments(new IdentitySegment('developers', [
+            'some_id',
+        ]));
+
+        $strategy = new EnableByMatchingIdentityId($segments);
+        self::assertFalse($strategy->isSatisfiedBy(new Identity('some_other_id')));
+    }
+
+    public function testItShouldBeSerializedAsArray(): void
+    {
+        $segments = new Segments(new IdentitySegment('developers', [
+            'some_id',
+        ]));
+
+        $strategy = new EnableByMatchingIdentityId($segments);
+        self::assertSame([
+            'type' => EnableByMatchingIdentityId::NAME,
+            'segments' => [
+                [
+                    'id'  => 'developers',
+                    'criteria' => ['some_id']
+                ]
+            ],
+        ], $strategy->jsonSerialize());
+    }
+}
diff --git a/packages/toggle-model/test/EnableByMatchingSegmentTest.php b/packages/toggle-model/test/EnableByMatchingSegmentTest.php
new file mode 100644
index 0000000..e988409
--- /dev/null
+++ b/packages/toggle-model/test/EnableByMatchingSegmentTest.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pheature\Test\Model\Toggle;
+
+use Pheature\Core\Toggle\Read\Segments;
+use Pheature\Model\Toggle\EnableByMatchingSegment;
+use Pheature\Model\Toggle\Identity;
+use Pheature\Model\Toggle\Segment;
+use PHPUnit\Framework\TestCase;
+
+final class EnableByMatchingSegmentTest extends TestCase
+{
+    /** @dataProvider getSegmentsCriteria */
+    public function testItShouldBeSatisfiedByMatchingSegment(array $criteria): void
+    {
+        $segments = new Segments(new Segment('users_from_barcelona', $criteria));
+
+        $strategy = new EnableByMatchingSegment($segments);
+        self::assertTrue($strategy->isSatisfiedBy(new Identity('some_id', $criteria)));
+    }
+
+    public function testItShouldNotBeSatisfiedByUnMatchingSegment(): void
+    {
+        $segments = new Segments(new Segment('users_from_barcelona', [
+            'location' => 'barcelona',
+        ]));
+
+        $strategy = new EnableByMatchingSegment($segments);
+        self::assertFalse($strategy->isSatisfiedBy(new Identity('some_id', [
+            'location' => 'bilbo',
+        ])));
+    }
+
+    public function testItShouldNotBeSatisfiedWithoutAnySegment(): void
+    {
+        $segments = new Segments();
+
+        $strategy = new EnableByMatchingSegment($segments);
+        self::assertFalse($strategy->isSatisfiedBy(new Identity('some_id', [
+            'location' => 'bilbo',
+        ])));
+    }
+
+    public function testItShouldNotBeSatisfiedWithoutAnyCriteria(): void
+    {
+        $segments = new Segments(new Segment('users_from_barcelona', []));
+
+        $strategy = new EnableByMatchingSegment($segments);
+        self::assertFalse($strategy->isSatisfiedBy(new Identity('some_id', [
+            'location' => 'bilbo',
+        ])));
+    }
+
+    public function testItShouldBeSerializedAsArray(): void
+    {
+        $segments = new Segments(new Segment('users_from_barcelona', [
+            'location' => 'barcelona',
+        ]));
+
+        $strategy = new EnableByMatchingSegment($segments);
+        self::assertSame([
+            'type' => EnableByMatchingSegment::NAME,
+            'segments' => [
+                [
+                    'id'  => 'users_from_barcelona',
+                    'criteria' => [
+                        'location' => 'barcelona',
+                    ],
+                ]
+            ],
+        ], $strategy->jsonSerialize());
+    }
+
+    public function getSegmentsCriteria(): array
+    {
+        return [
+            [
+                [
+                    'location' => 'barcelona',
+                ]
+            ],
+            [
+                [
+                    'top_buyers' => true,
+                    'location' => 'madrid',
+                ]
+            ],
+            [
+                [
+                    'free_shipping' => true,
+                    'top_buyers' => false,
+                    'location' => 'bilbo',
+                ]
+            ],
+        ];
+    }
+}
diff --git a/packages/toggle-model/test/StrategyFactoryTest.php b/packages/toggle-model/test/StrategyFactoryTest.php
new file mode 100644
index 0000000..157e849
--- /dev/null
+++ b/packages/toggle-model/test/StrategyFactoryTest.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Pheature\Test\Model\Toggle;
+
+use Pheature\Core\Toggle\Exception\InvalidStrategyTypeGiven;
+use Pheature\Model\Toggle\EnableByMatchingIdentityId;
+use Pheature\Model\Toggle\EnableByMatchingSegment;
+use Pheature\Model\Toggle\StrategyFactory;
+use PHPUnit\Framework\TestCase;
+
+final class StrategyFactoryTest extends TestCase
+{
+    private const STRATEGY_ID = 'an_strategy_id';
+    private const SEGMENT_MATCHING_STRATEGY_TYPE = 'enable_by_matching_segment';
+    private const IDENTITY_MATCHING_STRATEGY_TYPE = 'enable_by_matching_identity_id';
+
+    public function testItShouldKnownAvailableStrategyTypes(): void
+    {
+        $factory = new StrategyFactory();
+
+        self::assertSame([
+            self::SEGMENT_MATCHING_STRATEGY_TYPE,
+            self::IDENTITY_MATCHING_STRATEGY_TYPE,
+        ], $factory->types());
+    }
+
+    public function testItShouldThrowAnExceptionWithUnknownStrategyTypes(): void
+    {
+        $this->expectException(InvalidStrategyTypeGiven::class);
+        $factory = new StrategyFactory();
+
+        $factory->create(self::STRATEGY_ID, 'unknown_strategy_type');
+    }
+
+    public function testItShouldCreateInstancesOfEnableByMatchingSegmentStrategy(): void
+    {
+        $factory = new StrategyFactory();
+
+        $strategy = $factory->create(self::STRATEGY_ID, self::SEGMENT_MATCHING_STRATEGY_TYPE);
+        self::assertInstanceOf(EnableByMatchingSegment::class, $strategy);
+    }
+
+    public function testItShouldCreateInstancesOfEnableByMatchingIdentityStrategy(): void
+    {
+        $factory = new StrategyFactory();
+
+        $strategy = $factory->create(self::STRATEGY_ID, self::IDENTITY_MATCHING_STRATEGY_TYPE);
+        self::assertInstanceOf(EnableByMatchingIdentityId::class, $strategy);
+    }
+}