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

add inGroup(s) methods to Group #7046

Merged

Conversation

andrewandante
Copy link
Contributor

No description provided.

* @param string|int|Group $group
* @return bool
*/
public function inGroup($group) {
Copy link
Contributor

@tractorcow tractorcow Jun 26, 2017

Choose a reason for hiding this comment

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

Would be good if the phpdoc told me what this method did. :)

Is this checking that the provided group is IN the invoked group, or the other way around? Does this work recursively?

Tests would also be nice.

} elseif ($group instanceof Group) {
$groupCheckObj = $group;
} else {
user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
Copy link
Contributor

Choose a reason for hiding this comment

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

InvalidArgumentException is preferred response in this case.

user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
}

if(!$groupCheckObj) return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

psr-2 will fail here. All conditionals need { }

$groupCheckObj = Group::get()->byID($group);
} elseif (is_string($group)) {
$SQL_group = Convert::raw2sql($group);
$groupCheckObj = Group::get_one("\"Code\" = '{$SQL_group}'");
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use ORM filter API instead of raw SQL please.

@andrewandante
Copy link
Contributor Author

Tests added, and other feedback addressed @tractorcow - thanks for taking the time to look at it

$groupCheckObj = Group::get()->byID($group);
} elseif (is_string($group)) {
$SQL_group = Convert::raw2sql($group);
$groupCheckObj = Group::get()->filter(['Code' => $SQL_group])->first();
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't raw2sql when using filter api; It'll double encode.

Group::get()->filter('Code', $group)->first();

is all you need

* @param string|int|Group $group Group instance, Group Code or ID
* @return bool Returns TRUE if the Group is a child of the given group, otherwise FALSE
*/
public function inGroup($group)
Copy link
Contributor

Choose a reason for hiding this comment

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

Edge case, is $group->inGroup($group) ?

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 think it should return true. Added a test.

* Check if the group is a child of the given groups or any parent groups
*
* @param array $groups
* @param bool $strict set to TRUE if must be in ALL groups, or FALSE if must be in ANY
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a well named arg; $strict means something other than what you're using it for. Did you mean to flag as conjunctive (and) or disjunctive (or)? Perhaps $conjunctive = false is better?

* @param bool $strict set to TRUE if must be in ALL groups, or FALSE if must be in ANY
* @return bool Returns TRUE if the Group is a child of any of the given groups, otherwise FALSE
*/
public function inGroups($groups, $strict = false)
Copy link
Contributor

@tractorcow tractorcow Jun 27, 2017

Choose a reason for hiding this comment

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

Edge case: is $group->inGroups([]); true? Does it depend on whether the search is conjunctive or disjunctive?

public function inGroups($groups, $strict = false)
{
foreach ($groups as $group) {
if ($this->inGroup($group)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

you will be calling $this->collateAncestorIDs() once for each loop. Why not inline inGroup() into this method and optimise it a little better? You could create a set of all groupIDs, and another set of collateAncestorIDs(), then do subset comparison (array_intersect). You'd just need to count the intersection to determine if inGroups is true (intersect > 0 for disjunctive search, intersect === total for conjunctive search).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have reworked this, I think I understood what you meant? Let me know if I've gone down the wrong rabbithole 🐰

protected function identifierToGroup($group)
{
if (is_numeric($group)) {
$groupCheckObj = Group::get()->byID($group);
Copy link
Contributor

Choose a reason for hiding this comment

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

You are converting the id to a group, and then to an id again. That's a very inefficient SQL overhead.

$candidateIDs[] = $candidate->ID;
}
if ($requireAll) {
return array_intersect($candidateIDs, $ancestorIDs) === $ancestorIDs;
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorting issues [1, 2] !== [2, 1]. You should only need to measure size of each group.

You should also be comparing to $candidateIDs not to $ancestorIDs, since $ancestorIDs may contain groups that aren't being compared to.

if (is_numeric($group)) {
$groupCheckObj = Group::get()->byID($group);
} elseif (is_string($group)) {
$groupCheckObj = Group::get()->filter('Code', $group)->first();
Copy link
Contributor

Choose a reason for hiding this comment

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

If you require a group somecode that doesn't exist, your code will silently ignore it and pass.

public function testGroupInGroupExceptionForFakeGroupID()
{
$group = new Group();
$group->inGroup(99999999);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure an invalid argument exception should be thrown when a correct type is passed

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it should be an exception, but the wrong exception message is thrown. It says "invalid type" when the type is valid, but the ID isn't a real record.

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've updated the exception to cover all the bases - i.e. must be $correctType and exist

*/
protected function identifierToGroupID($group)
{
if (is_numeric($group) && Group::get()->byID($group)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the extra query needed here?

maybe $group->inGroup(999999); should just be false in this case, rather than an error, to save a whole query.

My earlier comment about checking that the group exists was intended only to check the result of a query if you had already performed it, since there's no new overhead anyway.

if (is_numeric($group) && Group::get()->byID($group)) {
return $group;
} elseif (is_string($group) && Group::get()->filter('Code', $group)->first()) {
return Group::get()->filter('Code', $group)->first()->ID;
Copy link
Contributor

Choose a reason for hiding this comment

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

You are running this entire SQL query twice? Can you not put this into a temp variable.

Copy link
Contributor

@dhensby dhensby left a comment

Choose a reason for hiding this comment

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

I think trying to have a custom error message for every type of invalid argument that's sent in is overkill. It'll be less logic and code just to use a generic message for the exceptions.

Lets keep code complexity down

if ($group) {
return $group->ID;
}
if ($groupID instanceof DataObject) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This fails to catch objects that don't extend from DataObject

Copy link
Contributor

@tractorcow tractorcow left a comment

Choose a reason for hiding this comment

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

You've been diligent keeping up with this PR so I'm going to be diligent with review.

foreach ($groups as $group) {
try {
$candidateIDs[] = $this->identifierToGroupID($group);
} catch (\InvalidArgumentException $e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If you're catching and returning false, then it's not really an error. Exceptions add alot of overhead (processing time) and shouldn't be used unless it's a real error.

Copy link
Contributor

Choose a reason for hiding this comment

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

Going back to @dhensby 's feedback

I think trying to have a custom error message for every type of invalid argument that's sent in is overkill. It'll be less logic and code just to use a generic message for the exceptions.
Lets keep code complexity down

It's too hard to get this right just drop exceptions and handle the logic here. If it's $requireAll and null, return false, otherwise if it's null and not $requireAll, don't add it to $candidateIDs. you'll need to move your empty() check to below this loop though, if you could end up with empty $candidateIDs.

$matches = array_intersect($candidateIDs, $ancestorIDs);
return count($candidateIDs) === count($matches);
}
return count(array_intersect($candidateIDs, $ancestorIDs)) > 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

same expression repeated above. Why not count($matches) > 0?

if ($requireAll) {
return count($candidateIDs) === count($matches);
}
return count($matches) > 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

!empty()

@dhensby dhensby merged commit c69a565 into silverstripe:master Jun 30, 2017
@tractorcow
Copy link
Contributor

Thanks @dhensby and @andrewandante

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.

3 participants