--- title: "Pattern Matching in PHP" image: /images/pattern-matching.png thumbnail: /images/thumbnail-pattern-matching.png code: https://gitlab.com/thibauddauce/pattern-matching description: How to build a pattern matching library in PHP? lang: en website: dev --- I'm a big fan of Haskell and one of my favorite feature of this awesome language is pattern matching. If you don't know what's pattern matching, it's this: ```hs data Customer = Student | Individual getPrice :: Customer -> Int getPrice Student = 10 getPrice Individual = 30 ``` The main advantage is, if I add a possibility in my type, for example `data Customer = Student | Individual | Company`, the Haskell compiler will tell me that my function is missing a pattern. I will need to provide a `getPrice Company = ?`. Of course, in PHP we don't have a compiler but we can still have some checks… ### The problem Imagine I have a class for my customer: ```php type === self::STUDENT) { return 10; } elseif ($this->type === self::INDIVIDUAL) { return 30; } else { throw new InvalidArgumentException('Neither student nor individual.'); } } } ``` By the way, pay attention of how 4 lines of code became 18 lines. But, of course, Haskell is a incomprehensible language ;-) If I have my unit tests like these: ```php $customer = new Customer; // in real life, I would use a name constructor… $customer->type = Customer::STUDENT; $this->assertEquals(10, $customer->getPrice()); ``` ```php $customer = new Customer; $customer->type = Customer::INDIVIDUAL; $this->assertEquals(30, $customer->getPrice()); ``` If latter, I add a new type `const COMPANY = 'company'` in my class, my tests will remain green. If I use this `if() {} elseif () {} else {}` in a lot of places, it will be difficult to catch all occurrences. ### The solution Use [my small pattern matching library](https://gitlab.com/thibauddauce/pattern-matching) (or build your own, it's only a mater of hours…): ```php match($this->type, $actions); } public function getPrice() { $this->patternMatchOnType([ self::STUDENT => 10, self::INDIVIDUAL => 30, ]); } } ``` If I add a new type and I change my enumerate definition in one place `new Pattern([self::STUDENT, self::INDIVIDUAL, self::COMPANY])`, my previous tests are now failing with a new exception: ```html MissingPatternsDuringMatch: 'company' was missing during the match. Expected patterns were 'student', 'individual', 'company' and received patterns were 'student', 'individual'. ``` Even if, I never wrote a test to check if the price for a company is correct. ### Extra features of my library You can check [the test file](https://gitlab.com/thibauddauce/pattern-matching/blob/master/tests/PatternTest.php) for all features. #### Checks My `match()` function is three simple checks happening every time. It means, it raises an exception even if the pattern could be resolved. - there is no missing pattern in the array (all cases must have a response) - there is no extra pattern in the array (if a type is removed, I want to remove all the occurrences of the dead code) - the value provided is in the pattern list #### Callbacks If your application is doing expensive work for each pattern (more expensive than returning "10" or "30"), you can wrap the result in a callback: ```php $this->patternMatchOnType([ self::STUDENT => function() { return StudentPrice::fetchFromFile(); }, self::INDIVIDUAL => function() { return Price::where('type', self::INDIVIDUAL)->first()->value; }, ]); ``` #### Callbacks arguments And you can also give arguments to your callbacks with `with`: ```php public function patternMatchOnType(array $actions) { return (new Pattern([self::STUDENT, self::INDIVIDUAL])) ->with($this) // The callbacks will receive the instance of the customer ->match($this->type, $actions); } public function getPrice() { $this->patternMatchOnType([ self::STUDENT => 10, self::INDIVIDUAL => function(Customer $customer) { return IndividualPrice::where('age', '>', $customer->age)->first()->value; }, ]); } ``` Even if, in this case, you could simply use `$this->age` inside the callback.