Examples

Let's put everything from the last three chapters together, shall we?

Here is an example implementation of a Loader and a MultiStrategy.

namespace My;

use Caridea\Acl\Target;
use Caridea\Acl\RuleAcl;
use Caridea\Acl\Rule;
use Caridea\Acl\Service;

/**
 * Loads ACLs for \My\Foobar entities.
 */
class FoobarLoader extends \Caridea\Acl\AbstractMultiLoader
{
    /**
     * @var \My\FoobarDao
     */
    private $dao;

    /**
     * Creates a new \My\FoobarLoader
     *
     * @param \My\FoobarDao $dao  The DAO
     */
    public function __construct(FoobarDao $dao)
    {
        $this->dao = $dao;
    }

    /**
     * {@inheritDoc}
     */
    public function supports(Target $target)
    {
        return $target->getType() == 'foobar';
    }

    /**
     * {@inheritDoc}
     */
    public function loadAll(array $targets, array $subjects, Service $service): array
    {
        $acls = [];
        if (!empty($targets)) {
            // Gather up all our record IDs.
            // Let's make sure all are supported Targets.
            $ids = [];
            foreach ($targets as $target) {
                if (!$this->supports($target)) {
                    throw new \InvalidArgumentException("Unsupported type: " . $target->getType());
                }
                $ids[] = $target->getId();
            }

            // Load all entities.
            // Let's say this method returns an array keyed by ID.
            $entities = $this->dao->someMethodToLoadSeveralEntitiesByIds($ids);

            // Fill in all ACLs
            foreach ($targets as $target) {
                $entity = $entities[$target->getId()] ?? null;
                if ($entity === null) {
                    throw new \Caridea\Acl\Exception\Unloadable("Could not load record: " . $target->getId());
                }
                $acls[(string) $target] = $this->loadAcl($entity, $target, $subjects, $service);
            }
        }
        return $acls;
    }

    /**
     * Loads the actual ACL.
     *
     * @param \My\Foobar $entity
     * @param \Caridea\Acl\Target $target
     * @param \Caridea\Acl\Subject[] $subjects
     * @param \Caridea\Acl\Service $service
     * @return \Caridea\Acl\RuleAcl
     */
    protected function loadAcl(Foobar $entity, Target $target, array $subjects, Service $service)
    {
        // load the parent record's ACL
        $parent = $entity->getParent() === null ?
            $service->get(new Target('foobar', $entity->getParent()), $subjects)
            : null;

        // create the rules
        $rules = [];
        foreach ($subjects as $subject) {
            if ($subject->getType() == 'role' && $subject->getId() == 'admin') {
                // allow "admin" role for all permissions
                $rules[] = Rule::allow($subject);
            } elseif ($subject->getType() == 'role' && $subject->getId() == 'user') {
                // allow "user" role the read permission
                $rules[] = Rule::allow($subject, ['read']);
            } elseif ($subject->getType() == 'principal' && $subject->getId() == $record->getOwner()) {
                // allow the record owner CRUD permissions
                $rules[] = Rule::allow($subject, ['create', 'read', 'update', 'delete']);
            }
        }

        // return the final constructed ACL
        return new RuleAcl($target, $subjects, $rules, $parent);
    }
}

Now, let's see how it all works together.

// A list of all of your custom loaders
$loaders = [
    new \My\FoobarLoader($foobarDao),
];

// Use the Cache Strategy to cache lookups
$strategy = new \Caridea\Acl\CacheStrategy(
    new \Caridea\Acl\DelegateStrategy($loaders);
);
$service = new \Caridea\Acl\Service($strategy);

// determine which subjects the user has
$subjects = MyClass::getSubjects();
$target = new Target('foobar', 123);

$allowed = $service->can($subjects, 'delete', $target);

try {
    $service->assert($subjects, 'delete', $target);
} catch (\Caridea\Acl\Exception\Forbidden $e) {
    // not allowed!
}