Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

[WIP] [ParamConverter] add 'with' option to hydrate related object #153

Closed
wants to merge 3 commits into from

3 participants

@jaugustin

Hi,

This add a new option with for the PropelParamConverter

This allow to hydrate related object simply by adding option={"with": {"book"}}

<?php
/**
 * @ParamConverter("post", class="BlogBundle\Model\Post", options={"with"={ {"comments", "left join"} }})
 */
public function myAction(Post $post) { }

and when sensiolabs/SensioFrameworkExtraBundle#119 is accepted :

<?php
/**
 * @ParamConverter("post", options={"with"={"comments"}})
 */
public function myAction(Post $post) { }
@jaugustin

this need to be tested ;)

@travisbot

This pull request passes (merged ccb87491 into 6a648ed).

@willdurand
Owner

I made changes in the documentation last night, can you update? :)

@jaugustin

I will :) and add some tests.

@jaugustin

I will rebase this pr when #156 is merged

@willdurand
Owner

good to rebase :)

@jaugustin

Test are failling when hydrating one-to-many relation, I don't know why.

Book * --- 1 Author

If I use book has parameter and hydrate author it work,

If I use author has parameter and hydrate books, there is only one book hydrated :(

@travisbot

This pull request fails (merged b5c98ac4 into 64a6b02).

@travisbot

This pull request fails (merged 978a6fd into 64a6b02).

@willdurand
Owner

Merged, thanks!

@willdurand willdurand closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
111 Request/ParamConverter/PropelParamConverter.php
@@ -41,6 +41,14 @@ class PropelParamConverter implements ParamConverterInterface
*/
protected $exclude = array();
+ /**
+ * list of with option use to hydrate related object
+ * @var array
+ */
+ protected $withs;
+
+ protected $hasWith = false;
+
public function apply(Request $request, ConfigurationInterface $configuration)
{
$classQuery = $configuration->getClass() . 'Query';
@@ -75,6 +83,8 @@ public function apply(Request $request, ConfigurationInterface $configuration)
$this->filters = $request->attributes->all();
}
+ $this->withs = isset($options['with'])? is_array($options['with'])? $options['with'] : array($options['with']) : array();
+
// find by Pk
if (false === $object = $this->findPk($classQuery, $request)) {
// find by criteria
@@ -97,18 +107,52 @@ public function apply(Request $request, ConfigurationInterface $configuration)
return true;
}
+ public function supports(ConfigurationInterface $configuration)
+ {
+ if (null === ($classname = $configuration->getClass())) {
+ return false;
+ }
+ if (!class_exists($classname)) {
+ return false;
+ }
+ // Propel Class?
+ $class = new \ReflectionClass($configuration->getClass());
+ if ($class->isSubclassOf('BaseObject')) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Try to find the object with the id
+ *
+ * @param string $classQuery the query class
+ * @param Request $request
+ */
protected function findPk($classQuery, Request $request)
{
if (in_array($this->pk, $this->exclude) || !$request->attributes->has($this->pk)) {
return false;
}
- return $classQuery::create()->findPk($request->attributes->get($this->pk));
+ if (!$this->hasWith) {
+ return $this->getQuery($classQuery)->findPk($request->attributes->get($this->pk));
+ } else {
+ return reset($this->getQuery($classQuery)->filterByPrimaryKey($request->attributes->get($this->pk))->find());
+ }
}
+ /**
+ * Try to find the object with all params from the $request
+ *
+ * @param string $classQuery
+ * @param Request $request the query class
+ * @param array $exclude an array of param to exclude from the filter
+ */
protected function findOneBy($classQuery, Request $request)
{
- $query = $classQuery::create();
+ $query = $this->getQuery($classQuery);
$hasCriteria = false;
foreach ($this->filters as $column => $value) {
if (!in_array($column, $this->exclude)) {
@@ -123,23 +167,62 @@ protected function findOneBy($classQuery, Request $request)
return false;
}
- return $query->findOne();
+ if (!$this->hasWith) {
+ return $query->findOne();
+ } else {
+ return reset($query->find());
+ }
}
- public function supports(ConfigurationInterface $configuration)
+ /**
+ * Init the query class with optional joinWith
+ *
+ * @param string $classQuery
+ * @throws \Exception
+ */
+ protected function getQuery($classQuery)
{
- if (null === ($classname = $configuration->getClass())) {
- return false;
- }
- if (!class_exists($classname)) {
- return false;
+ $query = $classQuery::create();
+
+ foreach ($this->withs as $with) {
+ if (is_array($with)) {
+ if (2 == count($with)) {
+ $query->joinWith($with[0], $this->getValidJoin($with));
+ $this->hasWith = true;
+ } else {
+ throw new \Exception(sprintf('ParamConverter : "with" parameter "%s" is invalid,
+ only string relation name (e.g. "Book") or an array with two keys (e.g. {"Book", "LEFT_JOIN"}) are allowed',
+ var_export($with, true)));
+ }
+ } else {
+ $query->joinWith($with);
+ $this->hasWith = true;
+ }
}
- // Propel Class?
- $class = new \ReflectionClass($configuration->getClass());
- if ($class->isSubclassOf('BaseObject')) {
- return true;
+
+ return $query;
+ }
+
+ /**
+ * Return the valid join Criteria base on the with parameter
+ *
+ * @param array $with
+ * @throws \Exception
+ */
+ protected function getValidJoin($with)
+ {
+ switch (trim(str_replace(array('_', 'JOIN'), '', strtoupper($with[1])))) {
+ case 'LEFT':
+ return \Criteria::LEFT_JOIN;
+ case 'RIGHT':
+ return \Criteria::RIGHT_JOIN;
+ case 'INNER':
+ return \Criteria::INNER_JOIN;
}
- return false;
+ throw new \Exception(sprintf('ParamConverter : "with" parameter "%s" is invalid,
+ only "left", "right" or "inner" are allowed for join option',
+ var_export($with, true)));
}
+
}
View
35 Resources/doc/param_converter.markdown
@@ -64,4 +64,39 @@ public function myAction(Post $post, $author)
}
```
+#### Hydrate related object ####
+
+You could hydrate related object with the "with" option:
+
+``` php
+<?php
+
+/**
+ * @ParamConverter("post", class="BlogBundle\Model\Post", options={"with"={"Comments"}})
+ */
+public function myAction(Post $post)
+{
+}
+```
+
+You can set multiple with ```"with"={"Comments", "Author", "RelatedPosts"}```.
+
+The default join is an "inner join" but you can configure it to be a left join, right join or inner join :
+
+``` php
+<?php
+
+/**
+ * @ParamConverter("post", class="BlogBundle\Model\Post", options={"with"={ {"Comments", "left join" } }})
+ */
+public function myAction(Post $post)
+{
+}
+```
+Accepted parmeters for join :
+
+ left, LEFT, left join, LEFT JOIN, left_join, LEFT_JOIN
+ right, RIGHT, right join, RIGHT JOIN, right_join, RIGHT_JOIN
+ inner, INNER, inner join, INNER JOIN, inner_join, INNER_JOIN
+
[Back to index](index.markdown)
View
149 Tests/Request/ParamConverter/PropelParamConverterTest.php
@@ -12,6 +12,8 @@
class PropelParamConverterTest extends TestCase
{
+ protected $con;
+
public function setUp()
{
parent::setUp();
@@ -20,6 +22,14 @@ public function setUp()
}
}
+ public function tearDown()
+ {
+ \Propel::enableInstancePooling();
+ if ($this->con) {
+ $this->con->useDebug(false);
+ }
+ }
+
public function testParamConverterSupport()
{
$paramConverter = new PropelParamConverter();
@@ -155,27 +165,140 @@ public function testParamConverterFindWithOptionalParam()
public function testParamConverterFindWithMapping()
{
- $paramConverter = new PropelParamConverter();
- $request = new Request(array(), array(), array('toto' => 1, 'book' => null));
+ $paramConverter = new PropelParamConverter();
+ $request = new Request(array(), array(), array('toto' => 1, 'book' => null));
$configuration = new ParamConverter(array('class' => 'Propel\PropelBundle\Tests\Fixtures\Model\Book',
'name' => 'book',
'options' => array('mapping' => array('toto' => 'id'))
- ));
- $paramConverter->apply($request, $configuration);
- $this->assertInstanceOf('Propel\PropelBundle\Tests\Fixtures\Model\Book',$request->attributes->get('book'),
+ ));
+ $paramConverter->apply($request, $configuration);
+ $this->assertInstanceOf('Propel\PropelBundle\Tests\Fixtures\Model\Book',$request->attributes->get('book'),
'param "book" should be an instance of "Propel\PropelBundle\Tests\Fixtures\Model\Book"');
}
- public function testParamConverterFindSlugWithMapping()
- {
- $paramConverter = new PropelParamConverter();
- $request = new Request(array(), array(), array('slugParam_special' => 'my-book', 'book' => null));
+ public function testParamConverterFindSlugWithMapping()
+ {
+ $paramConverter = new PropelParamConverter();
+ $request = new Request(array(), array(), array('slugParam_special' => 'my-book', 'book' => null));
$configuration = new ParamConverter(array('class' => 'Propel\PropelBundle\Tests\Fixtures\Model\Book',
'name' => 'book',
'options' => array('mapping' => array('slugParam_special' => 'slug'))
- ));
- $paramConverter->apply($request, $configuration);
- $this->assertInstanceOf('Propel\PropelBundle\Tests\Fixtures\Model\Book',$request->attributes->get('book'),
- 'param "book" should be an instance of "Propel\PropelBundle\Tests\Fixtures\Model\Book"');
+ ));
+ $paramConverter->apply($request, $configuration);
+ $this->assertInstanceOf('Propel\PropelBundle\Tests\Fixtures\Model\Book',$request->attributes->get('book'),
+ 'param "book" should be an instance of "Propel\PropelBundle\Tests\Fixtures\Model\Book"');
+ }
+
+ public function testParamConvertWithOptionWith()
+ {
+ $this->loadFixture();
+ \Propel::disableInstancePooling();
+
+ $paramConverter = new PropelParamConverter();
+ $request = new Request(array(), array(), array('id' => 1, 'book' => null));
+ $configuration = new ParamConverter(array(
+ 'class' => 'Propel\PropelBundle\Tests\Request\ParamConverter\MyBook',
+ 'name' => 'book',
+ 'options' => array(
+ 'with' => 'MyAuthor'
+ )
+ ));
+
+ $nb = $this->con->getQueryCount();
+ $paramConverter->apply($request, $configuration);
+
+ $book = $request->attributes->get('book');
+ $this->assertInstanceOf('Propel\PropelBundle\Tests\Request\ParamConverter\MyBook', $book,
+ 'param "book" should be an instance of "Propel\PropelBundle\Tests\Request\ParamConverter\MyBook"');
+
+ $this->assertEquals($nb +1, $this->con->getQueryCount(), 'only one query to get the book');
+
+ $this->assertInstanceOf('Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor', $book->getMyAuthor(),
+ 'param "book" should be an instance of "Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor"');
+
+ $this->assertEquals($nb +1, $this->con->getQueryCount(), 'no new query to get the author');
+ \Propel::enableInstancePooling();
+ }
+
+ public function testParamConvertWithOptionWithLeftJoin()
+ {
+ $this->loadFixture();
+ \Propel::disableInstancePooling();
+
+ $paramConverter = new PropelParamConverter();
+ $request = new Request(array(), array(), array('param1' => 10, 'author' => null));
+ $configuration = new ParamConverter(array(
+ 'class' => 'Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor',
+ 'name' => 'author',
+ 'options' => array(
+ 'with' => array(array('MyBook', 'left join')),
+ 'mapping' => array('param1' => 'id'),
+ )
+ ));
+
+ $nb = $this->con->getQueryCount();
+ $paramConverter->apply($request, $configuration);
+
+ $author = $request->attributes->get('author');
+ $this->assertInstanceOf('Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor', $author,
+ 'param "author" should be an instance of "Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor"');
+
+ $this->assertEquals($nb +1, $this->con->getQueryCount(), 'only one query to get the book');
+
+ $books = $author->getMyBooks();
+ $this->assertInstanceOf('PropelObjectCollection', $books);
+ $this->assertCount(2, $books, 'Author should have two books');
+
+ $this->assertEquals($nb +1, $this->con->getQueryCount(), 'no new query to get the books');
+ }
+
+ protected function loadFixture()
+ {
+ $this->loadPropelQuickBuilder();
+
+ $schema = <<<XML
+<database name="default" package="vendor.bundles.Propel.PropelBundle.Tests.Request.DataFixtures.Loader" namespace="Propel\PropelBundle\Tests\Request\ParamConverter" defaultIdMethod="native">
+ <table name="my_book">
+ <column name="id" type="integer" primaryKey="true" />
+ <column name="name" type="varchar" size="255" />
+ <column name="my_author_id" type="integer" />
+ <foreign-key foreignTable="my_author" onDelete="CASCADE" onUpdate="CASCADE">
+ <reference local="my_author_id" foreign="id" />
+ </foreign-key>
+ </table>
+
+ <table name="my_author">
+ <column name="id" type="integer" primaryKey="true" />
+ <column name="name" type="varchar" size="255" />
+ </table>
+
+</database>
+XML;
+ $builder = new \PropelQuickBuilder();
+ $builder->setSchema($schema);
+ if (class_exists('Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor')) {
+ $builder->setClassTargets(array());
+ }
+ $this->con = $builder->build();
+ $this->con->useDebug(true);
+
+ MyBookQuery::create()->deleteAll($this->con);
+ MyAuthorQuery::create()->deleteAll($this->con);
+
+ $author = new MyAuthor();
+ $author->setId(10);
+ $author->setName('Will');
+
+ $book = new MyBook();
+ $book->setId(1);
+ $book->setName('PropelBook');
+ $book->setMyAuthor($author);
+
+ $book2 = new MyBook();
+ $book2->setId(2);
+ $book2->setName('sf2lBook');
+ $book2->setMyAuthor($author);
+
+ $author->save($this->con);
}
}
Something went wrong with that request. Please try again.