Skip to content

Hansanghyeon/spec-pattern-php

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Specification pattern - Wikipedia

How to use

composer require hansanghyeon/spec

Example

<?php

declare(strict_types=1);

namespace Hansanghyeon\Spec;

final class Product
{
    public $isNew;
    public $name;
    public $price;
    public $color;

    public function __construct(array $data)
    {
        $this->isNew = $data['isNew'];
        $this->name = $data['name'];
        $this->price = $data['price'];
        $this->color = $data['color'];
    }
}
<?php

declare(strict_types=1);

use Hansanghyeon\Spec\Spec;
use Hansanghyeon\Spec\Product;
use PHPUnit\Framework\TestCase;

function pipe(...$functions)
{
    return function ($data) use ($functions) {
        return array_reduce($functions, function ($carry, $function) {
            return $function($carry);
        }, $data);
    };
}

class ProductSpec
{
    public static function getSpecs(): array
    {
        // 새롭게 추가된 상품인지 확인하는 스펙
        $isNewSpec = new Spec(function ($candidate) {
            return $candidate->isNew === true;
        });

        $isHighPriceSpec = new Spec(function ($candidate) {
            return $candidate->price >= 200_000;
        });

        // 새롭게 추가된 상품이 아니고 기존 상품이면서 상품의 가격이 200,000원 이상인지 확인하는 스펙
        $isOriginalAndHighPriceSpec = $isNewSpec->not()->and($isHighPriceSpec);

        return [
            'isNewSpec' => $isNewSpec,
            'isHighPriceSpec' => $isHighPriceSpec,
            'isOriginalAndHighPriceSpec' => $isOriginalAndHighPriceSpec,
        ];
    }
}

final class ProductSpecTest extends TestCase
{
    private $products;
    private $specs;

    protected function setUp(): void
    {
        parent::setUp();

        // 스펙 객체들을 스태틱 메서드를 통해 가져옴
        $this->specs = ProductSpec::getSpecs();

        $this->products = array(
            new Product([
                'isNew' => true,
                'name' => '피카츄',
                'price' => 100_000,
                'color' => 'black'
            ]),
            new Product([
                'isNew' => false,
                'name' => '라이츄',
                'price' => 150_000,
                'color' => 'red'
            ]),
            new Product([
                'isNew' => false,
                'name' => '파이리',
                'price' => 200_000,
                'color' => 'white'
            ]),
            new Product([
                'isNew' => true,
                'name' => '꼬북이',
                'price' => 250_000,
                'color' => 'black'
            ]),
        );
    }

    public function testIsNewSpecFiltering()
    {
        $isNewSpec = $this->specs['isNewSpec'];

        $filteredProducts = pipe(
            function ($products) use ($isNewSpec) {
                return array_filter($products, function ($product) use ($isNewSpec) {
                    return $isNewSpec->isSatisfiedBy($product);
                });
            },
            'array_values'
        )($this->products);

        $this->assertCount(2, $filteredProducts);
        // 피카츄와 꼬북이만이 isNew 스펙을 만족해야 합니다.
        $this->assertEquals('피카츄', $filteredProducts[0]->name);
        // $this->assertEquals('꼬북이', $filteredProducts[1]->name);
    }

    public function testIsHighPriceSpecFiltering()
    {
        $isHighPriceSpec = $this->specs['isHighPriceSpec'];

        $filteredProducts = pipe(
            function ($products) use ($isHighPriceSpec) {
                return array_filter($products, function ($product) use ($isHighPriceSpec) {
                    return $isHighPriceSpec->isSatisfiedBy($product);
                });
            },
            'array_values'
        )($this->products);

        $this->assertCount(2, $filteredProducts);
        // 파이리와 꼬북이만이 가격이 200,000원 이상을 만족해야 합니다.
        $this->assertEquals('파이리', $filteredProducts[0]->name);
        $this->assertEquals('꼬북이', $filteredProducts[1]->name);
    }

    public function testIsOriginalAndHighPriceSpecFiltering()
    {
        $isOriginalAndHighPriceSpec = $this->specs['isOriginalAndHighPriceSpec'];

        $filteredProducts = pipe(
            function ($products) use ($isOriginalAndHighPriceSpec) {
                return array_filter($products, function ($product) use ($isOriginalAndHighPriceSpec) {
                    return $isOriginalAndHighPriceSpec->isSatisfiedBy($product);
                });
            },
            'array_values'
        )($this->products);

        $this->assertCount(1, $filteredProducts);
        // 라이츄만이 새로운 상품이 아니고 가격이 200,000원 이상을 만족해야 합니다.
        $this->assertEquals('파이리', $filteredProducts[0]->name);
    }
}