Using a Doctrine EventListener to keep a record from being returned?

112 views
Skip to first unread message

jazzslider

unread,
Nov 13, 2009, 2:38:35 PM11/13/09
to DallasPHP
Hello!

Gonna post this to the official Doctrine mailing list as soon as my
membership there is approved, but I figured since we talked about
Doctrine this past week it's fair game to ask here too :)

In one of my projects, I'm trying to do application-level access
control on my Doctrine models. My access control system isn't
database-driven so I can't really include access control logic at the
query level …as a result, I'm having to perform the checks after the
query has returned all its data, like so:

[code]
// for single records...
$record = $doctrineTable->find($id);
if (!$acl->isAllowed($role, $record, 'read')) {
// Set $record to false so it will appear to later code
// as though the requested record was not found.
$record = false;
}

// for multiple records...
$records = $doctrineTable->findAll();
foreach ($records as $key => $record) {
if (!$acl->isAllowed($role, $record, 'read')) {
// Remove the record from the collection so it will
// not show up to later code.
$records->remove($key);
}
}
[/code]

Now, this works, but it gets pretty danged repetitive, and has a
tendency to muddy up my controller scripts significantly. I'd much
rather allow the model layer to handle this directly, and after Jake's
presentation on Tuesday I realized that Doctrine's event and record
listeners might be able to help.

I've managed to get things working for other kinds of operations
(create, update, and delete) by using the preInsert(), preUpdate(),
and preDelete() methods of Doctrine_Record_Listener like so:

[code]
class My_Doctrine_Record_Listener_Acl extends Doctrine_Record_Listener
{
public function __construct($acl, $currentRole);

public function preInsert(Doctrine_Event $event)
{
$resource = $event->getInvoker();
if (!$this->getAcl()->isAllowed($this->getCurrentRole(),
$resource, 'create')) {
throw new My_Model_Exception('You are not allowed to do
that.');
}
}

// and then preUpdate and preDelete too
}
[/code]

However, for the "read" operations in my first code sample, I'm having
trouble figuring out a couple of things. First off, I'm not sure what
listener method to implement…it's a bit difficult to tell which
listener is firing when, and the documentation is a bit terse.
Second, I'm not really sure what to do once I've got the listener
figured out …the desired behavior is that if a record isn't allowed
for the role defined in the listener object, the table will simply
return whatever it would've returned if that record didn't even exist
in the first place.

I've tried this out in the Doctrine_Record_Listener::postHydrate()
hook, since I figured that would definitely get called for every
record returned by any given SELECT query …but it doesn't always seem
to fire when I expect it to (e.g., if I call $table->findAll(), it
doesn't seem to be firing for the records being returned), and when it
does fire, I don't know how to tell it not to return the record…maybe
set $event->data to NULL, or call $event->skipOperation()?

Anyway, I thought I'd go ahead and ask if anyone had run into this
kind of use case before, or if there's a better approach I'm not
considering. Any ideas?

Thanks!
Adam

Jake Smith

unread,
Nov 16, 2009, 11:38:20 AM11/16/09
to dall...@googlegroups.com
Adam,

Have you tried the preDqlSelect?

You will need to extend the Doctrine_EventListener.

http://www.doctrine-project.org/documentation/manual/1_1/en/event-listeners#dql-hooks

Jake Smith
[t] @jakefolio
[w] www.jakefolio.com



--

You received this message because you are subscribed to the Google Groups "DallasPHP" group.
To post to this group, send email to dall...@googlegroups.com.
To unsubscribe from this group, send email to dallasphp+...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/dallasphp?hl=.



Adam Jensen

unread,
Nov 16, 2009, 1:43:04 PM11/16/09
to dall...@googlegroups.com
Jake,

Looked into that a bit, and put it into practice on, e.g., supplying a default orderBy clause for all select queries against a particular component…pretty cool stuff, really, since doing things like that at the listener level will likely allow me to get rid of an entire abstraction layer once I've got it all figured out.

However, what I'm looking to do in this instance is a bit different.  My access control logic is all in PHP (via Zend_Acl) rather than in the database, so what I need to be able to do is filter out impermissible results _after_ the query has already returned its result set.  It's not something I can really include in the query, unfortunately.

Any ideas on how to deal with that?

Thanks!
Adam

Jake Smith

unread,
Nov 16, 2009, 2:00:27 PM11/16/09
to dall...@googlegroups.com
Adam,

From a logic standpoint wouldn't you want to NOT return records/results if the user does not have permission to see?  Also, couldn't you do postDqlSelect if you want to handle this post select?


Jake Smith
[t] @jakefolio
[w] www.jakefolio.com


Adam Jensen

unread,
Nov 16, 2009, 2:26:00 PM11/16/09
to dall...@googlegroups.com
Jake,

Right, that's what I'd like to have happen.  Ideally at the controller layer I could just issue a typical $table->findAll() call, and then the event listener would filter out the impermissible records so that only the permissible ones were returned.

Doesn't look like there is a postDqlSelect() hook available in Doctrine_Record_Listener; that sounds like it'd be perfect, but I don't think it's available.

I did try using Doctrine_Record_Listener::postHydrate(), since within that context the $event->data property contains the fully-hydrated object (which is what my ACL implementation needs to see).  Problem is, I don't think postHydrate is really designed such that you can prevent the object from being returned altogether; also, it doesn't seem to run exactly when I'm expecting it to …I may be able to post an example of what I mean later, can't really work on it at the moment.

I'm also going to look into postFetchAll() later when I get the chance; looks promising, but I don't know if it'll have the hydrated object available yet.  Maybe I'm going about all this the wrong way :)

Thanks!
Adam

Jake Smith

unread,
Nov 16, 2009, 2:57:01 PM11/16/09
to dall...@googlegroups.com
Adam,

Have you tried postExec()?


Jake Smith
[t] @jakefolio
[w] www.jakefolio.com


Adam Jensen

unread,
Nov 16, 2009, 4:12:44 PM11/16/09
to dall...@googlegroups.com
Jake,

Haven't tried that one yet, no…was a bit confused about the difference between postExec, postExecute, and postStmtExecute as described in the docs at http://www.doctrine-project.org/documentation/manual/1_1/en/event-listeners#connection-listeners:query-execution-listeners …with such similar names, it's tough to know which one to try :)

I traced through some of the source, though, and it looks like the Doctrine_Connection::exec() method handles queries that change things (inserts, updates, deletes), while Doctrine_Connection::execute() handles queries that return things (selects).  So I'm guessing the hooks should work similarly, except that if you include prepared statement parameters, the *StmtExecute() hooks are required instead (if I'm reading the footnote in the manual correctly).  Does that all sound right?

If I get a chance tonight, I'll try a few more of these methods out…maybe in a testing environment where I can isolate exactly what's happening when…figuring out how all this works in the context of a full application is turning out to be a bit messy.

Meanwhile, I appreciate the help; pretty excited to get this working!

Thanks,
Adam

Adam Jensen

unread,
Nov 17, 2009, 7:28:20 AM11/17/09
to dall...@googlegroups.com
Hello!

OK, I did some experimentation on this yesterday evening, and I think I have a clearer sense of at least part of the problem.

First thing I needed to find out is which listeners were actually firing when, so I wrote two very simple listeners (an event listener and a record listener), each implementations of Doctrine_Overloadable (see last part of Creating a New Listener); they looked like this:

[code]
class My_Record_Listener_Tester extends Doctrine_Overloadable
{
  public function __call($methodName, $args)
  {
    echo "Doctrine_Record_Listener::$methodName" . PHP_EOL;
  }
}

// and then the same thing for Doctrine_EventListener
[/code]

I then wrote a simple CLI script to bootstrap my Doctrine manager and connection, attached these listeners as appropriate (Doctrine_Manager::addListener and Doctrine_Manager::addRecordListener), and ran the following test code:

[code]
echo "Running Doctrine_Connection::getTable('Post')..." . PHP_EOL;
$table = $conn->getTable('Post');
echo "...done." . PHP_EOL;

echo "Running Doctrine_Table::findAll()..." . PHP_EOL;
$records = $table->findAll();
echo "...done." . PHP_EOL;
[/code]

The first time I tried this, this is all that came out:

[output]
Running Doctrine_Connection::getTable('Post')...
...done.
Running Doctrine_Table::findAll()...
EventListener::preConnect
EventListener::postConnect
EventListener::preQuery
EventListener::postQuery
...done.
[/output]

Not a whole lot there to hook into; *Connect() won't help me, and the *Query() methods aren't really even documented.  But I was wondering at this point, why in the world were none of the hydration hooks running?  After all, I was getting back fully hydrated Post objects.

I took a closer look at my Post model at that point and realized that it's utilizing several built-in Doctrine templates ("actAs" stuff: timestampable, sluggable, etc.); since templates make heavy use of event and record listeners to do their work, I thought I'd test it without them attached, just to see if there were a conflict.  Sure enough, when I ran the script again:

[output]
Running Doctrine_Connection::getTable('Post')...
...done.
Running Doctrine_Table::findAll()...
Record_Listener::getOption
Record_Listener::preDqlSelect
EventListener::preConnect
EventListener::postConnect
EventListener::preQuery
EventListener::postQuery
Record_Listener::getOption
Record_Listener::preHydrate
Record_Listener::getOption
Record_Listener::postHydrate
Record_Listener::getOption
Record_Listener::preHydrate
Record_Listener::getOption
Record_Listener::postHydrate
...done.
[/output]

Suddenly there are a whole slew of Record_Listener events firing that weren't firing when the actAs templates were still attached.  That hydration sequence (getOption, preHydrate, getOption, postHydrate) fires once for every record returned…and there's also a getOption, preDqlSelect sequence that fires before the *Query() events.

Unfortunately, those actAs templates are pretty important, and so far I haven't found a way to get all those extra hooks to fire if _even one_ template is still attached to the model.

Do you know if this is a documented behavior?  If so, is there a workaround?

I'm going to cross-post this in the Doctrine list too, since I think it may be useful information …either that, or I'm missing something obvious :)

Thanks!
Adam

Jake Smith

unread,
Nov 17, 2009, 1:00:02 PM11/17/09
to dall...@googlegroups.com
Adam,

I would submit it as a bug.  It is either a bug or there is an undocumented way to handle such situation.
http://trac.doctrine-project.org/wiki/ReportBug


Jake Smith
[t] @jakefolio
[w] www.jakefolio.com


Adam Jensen

unread,
Nov 25, 2009, 10:52:59 AM11/25/09
to dall...@googlegroups.com
Thanks again for your help with this; I went through a couple of bug reports to find the answer, but I think I've got it working pretty smoothly now.

For anyone who's interested, I've blogged the complete solution at http://jazzslider.org/2009/11/25/using-zend-acl-with-doctrine-record-listeners

(As a sidenote: this blog is brand-new, and I'm not totally satisfied with its cross-browser support …so I apologize if it doesn't look quite right yet…but the content should all be there :)

Thanks again!
Adam

Jake Smith

unread,
Nov 25, 2009, 12:01:58 PM11/25/09
to dall...@googlegroups.com
Adam,

Not quite sure why, but your blog crashes my Safari on iPhone.....just FYI.


Jake Smith
[t] @jakefolio
[w] www.jakefolio.com


For more options, visit this group at http://groups.google.com/group/dallasphp?hl=en.

Adam Jensen

unread,
Nov 25, 2009, 2:14:53 PM11/25/09
to dall...@googlegroups.com
Hrmm…thought I'd fixed that, but it's happening for me too…I think it's related to my font-face definitions; guess I need to do some more testing. Thanks for letting me know!

Adam


Reply all
Reply to author
Forward
0 new messages