unicorn-fail/php-option

A highly extensible replacement for [phpoption/phpoption] with `TypedOption` support.

1.6.0 2019-08-07 08:57 UTC

This package is auto-updated.

Last update: 2024-04-08 04:56:16 UTC


README

A highly extensible replacement for phpoption/phpoption with TypedOption support.

Latest Version Total Downloads License PHP from Packagist

Build Status Codacy grade Codacy coverage phpcs coding standard

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*

None::create() Some::create()
PHP 5.3 0.000000539s 0.000010369s
PHP 5.4 0.000000427s 0.000007331s
PHP 5.5 0.000000422s 0.000007045s
PHP 5.6 0.000001036s 0.000006680s
PHP 7.0 0.000000185s 0.000002357s
PHP 7.1 0.000000124s 0.000002027s
PHP 7.2 0.000000127s 0.000001841s
PHP 7.3 0.000000111s 0.000001682s

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

📄 License

unicorn-fail/php-option is licensed under the Apache 2.0 license. See the LICENSE file for more details.