unicorn-fail / php-option
A highly extensible replacement for [phpoption/phpoption] with `TypedOption` support.
README
A highly extensible replacement for phpoption/phpoption with
TypedOption
support.
An Option
is intended for cases where you sometimes might return a value
(typically an object), and sometimes you might return no value (typically null)
depending on arguments, or other runtime factors.
Often times, you forget to handle the case where no value is returned. Not intentionally of course, but maybe you did not account for all possible states of the system; or maybe you indeed covered all cases, then time goes on, code is refactored, some of these your checks might become invalid, or incomplete. Suddenly, without noticing, the no value case is not handled anymore. As a result, you might sometimes get fatal PHP errors telling you that you called a method on a non-object; users might see blank pages, or worse.
On one hand, an Option
forces a developer to consciously think about both cases
(returning a value, or returning no value). That in itself will already make your code more
robust. On the other hand, the Option
also allows the API developer to provide
more concise API methods, and empowers the API user in how he consumes these methods.
๐ฆ Installation
This project can be installed via Composer:
$ composer require unicorn-fail/php-option
Basic Usage
Using Option
in your API
<?php use UnicornFail\PhpOption\None; use UnicornFail\PhpOption\Some; class MyRepository { public function findSomeEntity($criteria) { if (null !== $entity = $this->entityManager->find($criteria)) { return Some::create($entity); } // Use a singleton for the None case (it's statically cached for performance). return None::create(); } }
If you are consuming an existing library, you can also use a shorter version
which by default treats null
as None
, and everything else as Some
case:
After:
<?php use UnicornFail\PhpOption\Option; class MyRepository { public function findSomeEntity($criteria) { return Option::create($this->entityManager->find($criteria)); // Or, if you want to change the none value to false for example: return Option::create($this->em->find($criteria), ['noneValue' => false]); } }
Case 1: always require an Entity when invoking code
$entity = $repo->findSomeEntity($criteria)->get(); // Returns an Entity, or throws exception.
Case 2: fallback to default value if not available
$entity = $repo->findSomeEntity($criteria)->getOrElse(new Entity); // Or, if you need to lazily create the entity. $entity = $repo->findSomeEntity($criteria)->getOrCall(function() { return new Entity; });
No More Boiler Plate Code
Before:
$entity = $this->findSomeEntity(); if ($entity === null) { throw new NotFoundException(); } return $entity->name;
After:
return $this->findSomeEntity()->get()->name;
No more control flow exceptions
Before:
try { $entity = $this->findSomeEntity(); } catch (NotFoundException $ex) { $entity = new Entity; }
After:
$entity = $this->findSomeEntity()->getOrElse(new Entity);
Concise null handling
Before:
$entity = $this->findSomeEntity(); if ($entity === null) { return new Entity; } return $entity;
After:
return $this->findSomeEntity()->getOrElse(new Entity);
Chaining multiple alternative Options
If you need to try multiple alternatives, the orElse
method allows you to
do this very elegantly.
Before:
$entity = $this->findSomeEntity(); if ($entity === null) { $entity = $this->findSomeOtherEntity(); if ($entity === null) { $entity = $this->createEntity(); } } return $entity;
After:
return $this->findSomeEntity() ->orElse($this->findSomeOtherEntity()) ->orElse($this->createEntity());
The first option which is non-empty will be returned. This is especially useful with lazily evaluated options.
Lazily evaluated Options
The above example has a flaw where the option chain would need to evaluate all options when the method is called. This creates unnecessary overhead if the first option is already non-empty.
Fortunately, this can be easily solved by using LazyOption
which takes a callable
that will be invoked only if necessary:
use UnicornFail\PhpOption\LazyOption; return $this->findSomeEntity() ->orElse(LazyOption::create([$this, 'findSomeOtherEntity'])) ->orElse(LazyOption::create([$this, 'createEntity']));
Typed options
In cases where you need a specific PHP type returned (e.g. string, boolean, number, etc.) the TypedOption
class
may provide you with more flexibility:
Before:
// ?coords=32:43,35:22,94:33,95:34 $coordsStr = !!(isset($_GET['coords']) ? $_GET['coords'] : ''); $coords = $coordsStr ? array_map('trim', explode(',', $coordsStr)) : []; foreach ($coords as $coord) { list ($x, $y) = array_map('trim', explode(':', $coord)); $this->doSomething($x, $y); }
After:
use UnicornFail\PhpOption\TypedOption; // Automatically parsed by the SomeArray typed option. // ?coords=32:43,35:22,94:33,95:34 $items = TypedOption::pick($_GET, 'coords', ['keyDelimiter' => ':'])->getOrElse([]); foreach ($items as $x => $y) { $this->doSomething($x, $y); }
๐ API Documentation
Official and extensive API documentation coming soon (PRs are welcome).
๐ท๏ธ Versioning
SemVer is followed closely. Minor and patch releases should not introduce breaking changes to the codebase.
This project's initial release will start at version 1.6.0
to stay in line with existing phpoption/phpoption releases.
๐ ๏ธ Support & Backward Compatibility
Version <1.6.0
- This project will not patch any bugs, address any security related issues, or make another release.
Please upgrade, it should be as simple as:
$ composer remove phpoption/phpoption $ composer require unicorn-fail/php-option
Version >=1.6.0 <2.0.0
- This project will keep backward compatibility with phpoption/phpoption and continue running the original tests to ensure previous namespaces and implementation are still functional.
- The following classes are automatically registered as aliases so existing code should still remain functional (see known caveats below):
PhpOption\LazyOption
=>UnicornFail\PhpOption\LazyOption
PhpOption\None
=>UnicornFail\PhpOption\None
PhpOption\Option
=>UnicornFail\PhpOption\Option
PhpOption\Some
=>UnicornFail\PhpOption\Some
- The following methods have been deprecated, use their replacements instead:
Option::ensure()
=>Option::create()
Option::fromValue()
=>Option::create()
Option::fromArraysValue()
=>Option::pick()
Option::fromReturn()
=>Option::create()
$option->ifDefined()
=>$option->forAll()
- Known caveats:
- The original tests contain extremely minor alterations due testing/environment issues.
- PHP 5.3 has a weird bug where class aliases aren't being registered properly. Because of this, the classes had
to be extended from their respective replacements. This, unfortunately, prevents
\PhpOption\Some
,\PhpOption\None
and\PhpOption\LazyOption
from being able to be extended directly from\PhpOption\Option
. If your code implements anything resembling$option instanceof Option
, these will fail. You will need to change these to$option instance of \UnicornFail\PhpOption\OptionInterface
instead.
Version 2.0.0
- This project plans to remove support for all of
PHP 5
,PHP 7.0
,PHP 7.1
and backward compatibility with phpoption/phpoption.
โ Security
To report a security vulnerability, please use the Tidelift security contact. Tidelift will coordinate the fix and disclosure with us.
๐ทโโ๏ธ Contributing
Contributions to this library are welcome!
Please see CONTRIBUTING for additional details.
๐งช Testing
Local development (ignore changes to composer.json
):
$ composer require-test
$ composer test
With coverage:
$ composer require-test $ composer require-coverage $ composer test-coverage
๐ Performance Benchmarks
Of course, performance is important. Included in the tests is a performance benchmark which you can run on a machine of your choosing:
$ composer test-group performance
At its core, the overhead incurred by using Option
comes down to the time that it takes to
create one object, the Option
wrapper. It will also need to perform an additional method
call to retrieve the value from the wrapper. Depending on your use case(s) and desired
functionality, you may encounter varied results.
Average Overhead Per Invocation*
In the table above, these benchmarks rarely are well under a fraction of a microsecond. Many of them measure in nanoseconds; with newer PHP versions decreasing the overhead even more over time.
Unless you plan to call a method hundreds of thousands of times during a request, there is no reason
to stick to the object|null
return value; better give your code some options!
*Average based on the comparison of creating a single object vs. the creation of a wrapper and a single method call; iterated over 10000 times and then calculating the difference.
๐ฅ Credits & Acknowledgements
- Mark Carver (twitter) (github)
- Johannes (twitter) (github)
- All Contributors
๐ License
unicorn-fail/php-option is licensed under the Apache 2.0 license. See the LICENSE
file for more details.