├── .gitignore ├── README.md ├── composer.json ├── src ├── AndSpecification.php ├── CompositeSpecification.php ├── NotSpecification.php ├── OrSpecification.php └── SpecificationInterface.php └── tests ├── Mocks ├── InCollectionSpecification.php ├── NoticeNotSentSpecification.php └── OverDueSpecification.php ├── Test.php ├── db.json └── phpunit.xml /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Specification Pattern 2 | 3 | Sample implementation for the Specification Pattern in PHP, based on https://en.wikipedia.org/wiki/Specification_pattern. 4 | 5 | # Usage 6 | 7 | An example of how this might be used. Also check out the tests. 8 | 9 | ```php 10 | $invoices = /* load invoices from somewhere*/; 11 | $overDue = new OverDueSpecification(time()); 12 | $noticeNotSent = new NoticeNotSentSpecification(); 13 | $inCollection = new InCollectionSpecification(); 14 | 15 | // we want a list of invoices that need to be remitted 16 | // show all invoices that are overdue, no notice has been sent, and isn't in collection 17 | $needsToBeRemitted = $overDue 18 | ->andX($noticeNotSent) 19 | ->andX(new NotSpecification($inCollection)); 20 | 21 | $satisfying = array_filter($invoices, function($invoice) { 22 | return $needsToBeRemitted->isSatisfyableBy($invoice); 23 | }) 24 | ``` 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mbrevda/specifcation-pattern", 3 | "description": "A sample implementation of the Specification Pattern in PHP", 4 | "authors": [ 5 | { 6 | "name": "Moshe Brevda", 7 | "email": "mbrevda@gmail.com" 8 | } 9 | ], 10 | "minimum-stability": "dev", 11 | "require": {}, 12 | "require-dev": { 13 | "squizlabs/php_codesniffer": "2.*", 14 | "jakub-onderka/php-parallel-lint": "0.*", 15 | "phpunit/phpunit": "4.*" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Mbrevda\\SpecificationPattern\\": "src/", 20 | "Mbrevda\\SpecificationPattern\\Tests\\": "tests/" 21 | } 22 | }, 23 | "config": { 24 | "preferred-install": "dist", 25 | "optimize-autoloader": true, 26 | "process-timeout": 30 27 | 28 | }, 29 | "scripts": { 30 | "test": "vendor/bin/phpunit -c tests/phpunit.xml" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/AndSpecification.php: -------------------------------------------------------------------------------- 1 | specification1 = $specification1; 18 | $this->specification2 = $specification2; 19 | } 20 | 21 | public function isSatisfiedBy($candidate) 22 | { 23 | return $this->specification1->isSatisfiedBy($candidate) 24 | && $this->specification2->isSatisfiedBy($candidate); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/CompositeSpecification.php: -------------------------------------------------------------------------------- 1 | specification = $specification; 16 | } 17 | 18 | public function isSatisfiedBy($candidate) 19 | { 20 | return !$this->specification->isSatisfiedBy($candidate); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OrSpecification.php: -------------------------------------------------------------------------------- 1 | one = $one; 18 | $this->other = $other; 19 | } 20 | 21 | public function isSatisfiedBy($candidate) 22 | { 23 | return $this->one->isSatisfiedBy($candidate) 24 | || $this->other->isSatisfiedBy($candidate); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/SpecificationInterface.php: -------------------------------------------------------------------------------- 1 | isInCollection; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Mocks/NoticeNotSentSpecification.php: -------------------------------------------------------------------------------- 1 | hasSentNotice == false; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Mocks/OverDueSpecification.php: -------------------------------------------------------------------------------- 1 | date = $date; 14 | } 15 | 16 | public function isSatisfiedBy($candidate) 17 | { 18 | return $candidate->dueDate < $this->date; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Test.php: -------------------------------------------------------------------------------- 1 | invoices = json_decode(file_get_contents(__DIR__ . '/db.json')); 15 | $this->overDue = new OverDueSpecification(time()); 16 | $this->noticeNotSent = new NoticeNotSentSpecification(); 17 | $this->inCollection = new InCollectionSpecification(); 18 | } 19 | 20 | public function usingSpec($spec) 21 | { 22 | $res = []; 23 | foreach ($this->invoices as $invoice) { 24 | if ($spec->isSatisfiedBy($invoice)) { 25 | $res[] = $invoice; 26 | } 27 | } 28 | 29 | return $res; 30 | } 31 | 32 | public function testOverDueSpecification(){ 33 | $this->assertFalse($this->overDue->isSatisfiedBy($this->invoices[0])); 34 | $this->assertTrue($this->overDue->isSatisfiedBy($this->invoices[1])); 35 | $this->assertTrue($this->overDue->isSatisfiedBy($this->invoices[2])); 36 | $this->assertTrue($this->overDue->isSatisfiedBy($this->invoices[3])); 37 | } 38 | 39 | public function testNoticeSentSpecification(){ 40 | $this->assertTrue($this->noticeNotSent->isSatisfiedBy($this->invoices[0])); 41 | $this->assertTrue($this->noticeNotSent->isSatisfiedBy($this->invoices[1])); 42 | $this->assertFalse($this->noticeNotSent->isSatisfiedBy($this->invoices[2])); 43 | $this->assertTrue($this->noticeNotSent->isSatisfiedBy($this->invoices[3])); 44 | } 45 | 46 | public function testInCollectionSpecification(){ 47 | $this->assertFalse($this->inCollection->isSatisfiedBy($this->invoices[0])); 48 | $this->assertTrue($this->inCollection->isSatisfiedBy($this->invoices[1])); 49 | $this->assertFalse($this->inCollection->isSatisfiedBy($this->invoices[2])); 50 | $this->assertFalse($this->inCollection->isSatisfiedBy($this->invoices[3])); 51 | } 52 | 53 | public function testAndX() 54 | { 55 | $spec = $this->overDue->andX($this->noticeNotSent); 56 | $this->assertCount(2, $this->usingSpec($spec)); 57 | } 58 | 59 | public function testAndXandNot() 60 | { 61 | $spec = $this->overDue 62 | ->andX($this->noticeNotSent) 63 | ->not(new NotSpecification($this->inCollection)); 64 | $this->assertCount(2, $this->usingSpec($spec)); 65 | } 66 | 67 | public function testAndandNot() 68 | { 69 | $spec = $this->overDue 70 | ->andX($this->noticeNotSent) 71 | ->not(new NotSpecification($this->inCollection)); 72 | $this->assertCount(2, $this->usingSpec($spec)); 73 | } 74 | 75 | public function testOr() 76 | { 77 | $spec = $this->overDue 78 | ->orX(new NotSpecification($this->overDue)); 79 | 80 | $this->assertCount(4, $this->usingSpec($spec)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/db.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Pepsi Cola", 4 | "hasSentNotice": false, 5 | "dueDate": 2436368243, 6 | "isInCollection": false 7 | }, 8 | { 9 | "name": "Coca Cola", 10 | "hasSentNotice": false, 11 | "dueDate": 200, 12 | "isInCollection": true 13 | }, 14 | { 15 | "name": "Tempo", 16 | "hasSentNotice": true, 17 | "dueDate": 1255555, 18 | "isInCollection": false 19 | }, 20 | { 21 | "name": "Osem", 22 | "hasSentNotice": false, 23 | "dueDate": 200, 24 | "isInCollection": false 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | ./ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------