Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Traversal strategies #5

Merged
merged 11 commits into from
Jan 13, 2014
Merged

Conversation

danizord
Copy link
Contributor

@danizord danizord commented Jan 6, 2014

Finally! Sorry for the delay :D

Traversal strategies

The Graph traversal is a very complex problem, there are several ways to do this, but none of them is the best way for all cases. There are many cases that need a specific algorithm. For instance, If you have a very dense role graph, you'll probably want to avoid redundancy when traversing the graph (checking the same role more than once). But if you have a flatter role graph, then you will not care about the redundancy, because the logic to avoid redundancy would be more costly than the redundancy itself.

So, if we make this algorithm pluggable, the RBAC component will become much more flexible, allowing the developers to easily implement their own strategies for specific cases.

I have many ideas for other strategies to implement (e.g a non-redundant generator strategy), but for now I'll suggest two:

Built-in traversal strategies:

  • RecursiveRoleIteratorStrategy - The default strategy, it recursively traverses the roles using a SPL RecursiveIteratorIterator that iterate over instances of Rbac\Traversal\RecursiveRoleIterator.
  • GeneratorStrategy - This strategy requires PHP >= 5.5, the fastest and lightweight strategy! It takes advantage of the new PHP 5.5 generators to recursively traverse the role graph.

Benchmarks

I did some performance tests of the built-in strategies to know which one is faster. You can run the benchmarks yourself, just follow the instructions in the danizord/rbac-benchmarks repository. See the results on my PC:

daniel@Danibook:~/projects/rbac-benchmarks$ php vendor/bin/athletic -p src/ -b vendor/autoload.php

Danizord\RbacBenchmarks\Traversal\GeneratorStrategyEvent
    Method Name                   Iterations    Average Time      Ops/second
    ---------------------------  ------------  --------------    -------------
    flatFirstMatch             : [1,000     ] [0.0000078084469] [128,066.44072]
    flatLastMatch              : [1,000     ] [0.0000445151329] [22,464.27079]
    hierarchicalFirstDepthMatch: [1,000     ] [0.0000128533840] [77,800.52308]
    hierarchicalLastDepthMatch : [1,000     ] [0.0000390281677] [25,622.51979]


Danizord\RbacBenchmarks\Traversal\RecursiveIteratorStrategyEvent
    Method Name                   Iterations    Average Time      Ops/second
    ---------------------------  ------------  --------------    -------------
    flatFirstMatch             : [1,000     ] [0.0000188462734] [53,060.88783]
    flatLastMatch              : [1,000     ] [0.0000955798626] [10,462.45488]
    hierarchicalFirstDepthMatch: [1,000     ] [0.0000269591808] [37,093.11519]
    hierarchicalLastDepthMatch : [1,000     ] [0.0000535430908] [18,676.54602]

Additional changes

  • HierarchicalRoleInterface no longer extends RecursiveIterator, this means that the roles are not aware of traversal nor recursion anymore.
  • The recursive iteration that was previously in HierarchicalRole, now is done in the new RecursiveRoleIterator.
  • Rbac::isGranted() now now accepts a collection/array of RoleInterfaces, but still accepting a single RoleInterface instance. If one of the given roles has the required permission, then it returns true.
  • Some tests was updated, the code coverage of this component is now 100%. 😃

*/
public function __construct(TraversalStrategyInterface $strategy = null)
{
$this->traversalStrategy = ($strategy) ? $strategy : new RecursiveRoleIteratorStrategy;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be $this->traversalStrategy = $strategy ?: new RecursiveRoleIteratorStrategy();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If RecursiveRoleIteratorStrategy and GeneratorStrategy do exactly the same, you should add a check based on the PHP version, and use the most efficient one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bakura10 First, the $this->traversalStrategy must not be null. Second, I want to allow developers to use custom strategies, this is why I dont choose the strategy based on the PHP version.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructt $strategy ?: new Recursive() is equivalent to : if $straetgy is not null, use strategy, otherwise new Recursive.

It's a shortcut.

Yes I understood abut custom strategy, but you could do:

if (PHP_VER < 5.5) {  // Not the right syntax
    $this->traversalStrategy = $strategy ?: new RecursiveRoleIteratorStrategy;
} else {
    $this->traversalStrategy = $strategy ?: new GeneratorStrategy()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, nice! I'll make this changes tonight.

@bakura10
Copy link
Member

bakura10 commented Jan 6, 2014

Please note that tests are not passing on PHP 5.4 too. I think you'll have to skip some tests if not on PHP 5.5

@danizord
Copy link
Contributor Author

danizord commented Jan 6, 2014

I see, there is a standard way to do it?

@bakura10
Copy link
Member

bakura10 commented Jan 6, 2014

@danizord , I don't remember the exact syntax. Please check the ZF2 tests, do a search on the source code, I don't remember the exact syntax.

@bakura10
Copy link
Member

bakura10 commented Jan 6, 2014

I like the idea in overall.

The only thing I'm still not sure is really the Generator thing. This is just my opinion, but I still find the implementation a bit unusual (generators were clearly not thought for recursive it seems). Furthermore, we now have two implementations that do the same thing.

The performance question is here a bit tendentious: most of the time you have very few roles (even for a complex system I don't think you will have more than 10 roles), so the advantage is pretty negligeable. While I think it makes sense to have a polyfill for very critical components like the router (in this case you could have a loot of routes so the performance bump makes sense).

In this specific case, it seems to me like it is using generators for the sake of using generators. I really find the RecursiveIterator approach much more elegant for solving this issue.

Havign said that, I won't be against including it, but I'm not fond of it.

*/
public function __construct(TraversalStrategyInterface $strategy = null)
{
$this->traversalStrategy = $strategy ?: new RecursiveRoleIteratorStrategy;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't you using the GeneratorStrategy by default? It's faster and doesn't have problems with doctrine collections

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because generators are not supported on PHP 5.4

Envoyé de mon iPhone

Le 7 janv. 2014 à 12:05, arekkas notifications@github.com a écrit :

In src/Rbac/Rbac.php:

/
class Rbac
{
/
*

  • \* Determines if access is granted by checking the role and child roles for permission.
    
  • \* @var TraversalStrategyInterface
    
  • */
    
  • protected $traversalStrategy;
  • /**
  • \* @param null|TraversalStrategyInterface $strategy
    
  • */
    
  • public function __construct(TraversalStrategyInterface $strategy = null)
  • {
  •    $this->traversalStrategy = $strategy ?: new RecursiveRoleIteratorStrategy;
    
    Why aren't you using the GeneratorStrategy by default? It's faster and doesn't have problems with doctrine collections


Reply to this email directly or view it on GitHub.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a check for the PHP version here would help.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm waiting for the result of the discussion: "GeneratorStrategy is good or bad" to make sure if I should set it by default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please: $this->traversalStrategy = $strategy ?: new RecursiveRoleIteratorStrategy();

Note the () at the end. I know it makes no difference but... it's on my namespace :muhaha: .D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bakura10 done.

@aeneasr
Copy link
Contributor

aeneasr commented Jan 7, 2014

I like the idea to use the Strategy pattern for the graph traversal. What seems odd to me is that generators are faster than iterators, but then again, this is PHP so everything is possible ;)

In conclusion, I'm +1 with this!

/** @var RoleInterface $child */
if ($child->hasPermission($permission)) {
foreach ($iterator as $role) {
/* @var RoleInterface $role */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead, just consider $iterator a RoleInterface[]

That works even for return values in docblocks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct? $iterator is actually a Traversable and Traversable !== RoleInterface[]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danizord [] at the end of a type means "a traversable of "

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ocramius No, it means "an array of" AFAIK. So I'd suggest Traversable|RoleInterface[]. But I dont know if it will work as expected in PHPStorm. I'm using Sublime (:

@danizord
Copy link
Contributor Author

danizord commented Jan 7, 2014

@bakura10 This is not just for the sake of using generators. See, the RecursiveRoleIteratorStrategy needs about 3 classes with a lot of code (even extending ArrayIterator to reuse some code) to do the work, and the workflow is harder to undestard. While the GeneratorStrategy has just some lines of (readable) code in a single method class. So, I really dont see why it is not a good use case for generators. Consider:

  • Less classes,
  • Less lines of code,
  • More readable,
  • Faster.

generators were clearly not thought for recursive it seems

Why? https://github.com/nikic/iter/blob/master/src/iter.php#L490-500 😃

It almost solves the Doctrine collections incompatibility
return false;
}

if (empty($current->getChildren())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note that this only works on PHP 5.5, please do this as we need to support PHP 5.4:

$children = $current->getChildren();

if (empty($children)) {
    return false;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return ! empty($children);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You think I can remove it?
Or pass this responsibility to the role, e.g HierarchicalRoleInterface::hasChildren()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, getChildren will return $this->children, so yeah you can do return !empty($this->children)

@bakura10
Copy link
Member

bakura10 commented Jan 7, 2014

It looks pretty good to me, excepting the few namings we should fix. @Ocramius do you think this is good?

However I'll want you to also update ZfcRbac to use this code so that I can push both PR at the same time (I suppose this can break ZfcRbac no?)

@danizord
Copy link
Contributor Author

danizord commented Jan 9, 2014

The issue with Doctrine collections was solved! Updating PR description.

@bakura10
Copy link
Member

bakura10 commented Jan 9, 2014

Looks good to me.

Please could you update ZfcRbac so ZfcRbac does not stay in a zombie state after merging this?

@bakura10
Copy link
Member

bakura10 commented Jan 9, 2014

Please also update the various .dist file, as well as the tests where I have simple entities based on previous Rbac.

{
if (null !== $strategy) {
$this->traversalStrategy = $strategy;
} elseif (version_compare(PHP_VERSION, '5.5.0', '>=')) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks ugly =/ @bakura10 Don't you think a factory is the best place for this logic?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, use PHP_VERSION_ID instead.

I am with @danizord on this - the constructor argument should not be nullable (may be misunderstood) and injection should happen in a factory.

@bakura10
Copy link
Member

Ok, create a factory then

bakura10 added a commit that referenced this pull request Jan 13, 2014
@bakura10 bakura10 merged commit 1ee80ec into zf-fr:master Jan 13, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants