Skip to content
This repository

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

Closed
wants to merge 3 commits into from

3 participants

Jérémie Augustin Don't Add Me To Your Organization a.k.a The Travis Bot William Durand
Jérémie Augustin

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) { }
Jérémie Augustin

this need to be tested ;)

Don't Add Me To Your Organization a.k.a The Travis Bot

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

William Durand
Owner

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

Jérémie Augustin

I will :) and add some tests.

Jérémie Augustin

I will rebase this pr when #156 is merged

William Durand
Owner

good to rebase :)

Jérémie Augustin

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 :(

Don't Add Me To Your Organization a.k.a The Travis Bot

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

Don't Add Me To Your Organization a.k.a The Travis Bot

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

William Durand
Owner

Merged, thanks!

William Durand 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.
111 Request/ParamConverter/PropelParamConverter.php
@@ -41,6 +41,14 @@ class PropelParamConverter implements ParamConverterInterface
41 41 */
42 42 protected $exclude = array();
43 43
  44 + /**
  45 + * list of with option use to hydrate related object
  46 + * @var array
  47 + */
  48 + protected $withs;
  49 +
  50 + protected $hasWith = false;
  51 +
44 52 public function apply(Request $request, ConfigurationInterface $configuration)
45 53 {
46 54 $classQuery = $configuration->getClass() . 'Query';
@@ -75,6 +83,8 @@ public function apply(Request $request, ConfigurationInterface $configuration)
75 83 $this->filters = $request->attributes->all();
76 84 }
77 85
  86 + $this->withs = isset($options['with'])? is_array($options['with'])? $options['with'] : array($options['with']) : array();
  87 +
78 88 // find by Pk
79 89 if (false === $object = $this->findPk($classQuery, $request)) {
80 90 // find by criteria
@@ -97,18 +107,52 @@ public function apply(Request $request, ConfigurationInterface $configuration)
97 107 return true;
98 108 }
99 109
  110 + public function supports(ConfigurationInterface $configuration)
  111 + {
  112 + if (null === ($classname = $configuration->getClass())) {
  113 + return false;
  114 + }
  115 + if (!class_exists($classname)) {
  116 + return false;
  117 + }
  118 + // Propel Class?
  119 + $class = new \ReflectionClass($configuration->getClass());
  120 + if ($class->isSubclassOf('BaseObject')) {
  121 + return true;
  122 + }
  123 +
  124 + return false;
  125 + }
  126 +
  127 + /**
  128 + * Try to find the object with the id
  129 + *
  130 + * @param string $classQuery the query class
  131 + * @param Request $request
  132 + */
100 133 protected function findPk($classQuery, Request $request)
101 134 {
102 135 if (in_array($this->pk, $this->exclude) || !$request->attributes->has($this->pk)) {
103 136 return false;
104 137 }
105 138
106   - return $classQuery::create()->findPk($request->attributes->get($this->pk));
  139 + if (!$this->hasWith) {
  140 + return $this->getQuery($classQuery)->findPk($request->attributes->get($this->pk));
  141 + } else {
  142 + return reset($this->getQuery($classQuery)->filterByPrimaryKey($request->attributes->get($this->pk))->find());
  143 + }
107 144 }
108 145
  146 + /**
  147 + * Try to find the object with all params from the $request
  148 + *
  149 + * @param string $classQuery
  150 + * @param Request $request the query class
  151 + * @param array $exclude an array of param to exclude from the filter
  152 + */
109 153 protected function findOneBy($classQuery, Request $request)
110 154 {
111   - $query = $classQuery::create();
  155 + $query = $this->getQuery($classQuery);
112 156 $hasCriteria = false;
113 157 foreach ($this->filters as $column => $value) {
114 158 if (!in_array($column, $this->exclude)) {
@@ -123,23 +167,62 @@ protected function findOneBy($classQuery, Request $request)
123 167 return false;
124 168 }
125 169
126   - return $query->findOne();
  170 + if (!$this->hasWith) {
  171 + return $query->findOne();
  172 + } else {
  173 + return reset($query->find());
  174 + }
127 175 }
128 176
129   - public function supports(ConfigurationInterface $configuration)
  177 + /**
  178 + * Init the query class with optional joinWith
  179 + *
  180 + * @param string $classQuery
  181 + * @throws \Exception
  182 + */
  183 + protected function getQuery($classQuery)
130 184 {
131   - if (null === ($classname = $configuration->getClass())) {
132   - return false;
133   - }
134   - if (!class_exists($classname)) {
135   - return false;
  185 + $query = $classQuery::create();
  186 +
  187 + foreach ($this->withs as $with) {
  188 + if (is_array($with)) {
  189 + if (2 == count($with)) {
  190 + $query->joinWith($with[0], $this->getValidJoin($with));
  191 + $this->hasWith = true;
  192 + } else {
  193 + throw new \Exception(sprintf('ParamConverter : "with" parameter "%s" is invalid,
  194 + only string relation name (e.g. "Book") or an array with two keys (e.g. {"Book", "LEFT_JOIN"}) are allowed',
  195 + var_export($with, true)));
  196 + }
  197 + } else {
  198 + $query->joinWith($with);
  199 + $this->hasWith = true;
  200 + }
136 201 }
137   - // Propel Class?
138   - $class = new \ReflectionClass($configuration->getClass());
139   - if ($class->isSubclassOf('BaseObject')) {
140   - return true;
  202 +
  203 + return $query;
  204 + }
  205 +
  206 + /**
  207 + * Return the valid join Criteria base on the with parameter
  208 + *
  209 + * @param array $with
  210 + * @throws \Exception
  211 + */
  212 + protected function getValidJoin($with)
  213 + {
  214 + switch (trim(str_replace(array('_', 'JOIN'), '', strtoupper($with[1])))) {
  215 + case 'LEFT':
  216 + return \Criteria::LEFT_JOIN;
  217 + case 'RIGHT':
  218 + return \Criteria::RIGHT_JOIN;
  219 + case 'INNER':
  220 + return \Criteria::INNER_JOIN;
141 221 }
142 222
143   - return false;
  223 + throw new \Exception(sprintf('ParamConverter : "with" parameter "%s" is invalid,
  224 + only "left", "right" or "inner" are allowed for join option',
  225 + var_export($with, true)));
144 226 }
  227 +
145 228 }
35 Resources/doc/param_converter.markdown
Source Rendered
@@ -64,4 +64,39 @@ public function myAction(Post $post, $author)
64 64 }
65 65 ```
66 66
  67 +#### Hydrate related object ####
  68 +
  69 +You could hydrate related object with the "with" option:
  70 +
  71 +``` php
  72 +<?php
  73 +
  74 +/**
  75 + * @ParamConverter("post", class="BlogBundle\Model\Post", options={"with"={"Comments"}})
  76 + */
  77 +public function myAction(Post $post)
  78 +{
  79 +}
  80 +```
  81 +
  82 +You can set multiple with ```"with"={"Comments", "Author", "RelatedPosts"}```.
  83 +
  84 +The default join is an "inner join" but you can configure it to be a left join, right join or inner join :
  85 +
  86 +``` php
  87 +<?php
  88 +
  89 +/**
  90 + * @ParamConverter("post", class="BlogBundle\Model\Post", options={"with"={ {"Comments", "left join" } }})
  91 + */
  92 +public function myAction(Post $post)
  93 +{
  94 +}
  95 +```
  96 +Accepted parmeters for join :
  97 +
  98 + left, LEFT, left join, LEFT JOIN, left_join, LEFT_JOIN
  99 + right, RIGHT, right join, RIGHT JOIN, right_join, RIGHT_JOIN
  100 + inner, INNER, inner join, INNER JOIN, inner_join, INNER_JOIN
  101 +
67 102 [Back to index](index.markdown)
149 Tests/Request/ParamConverter/PropelParamConverterTest.php
@@ -12,6 +12,8 @@
12 12 class PropelParamConverterTest extends TestCase
13 13 {
14 14
  15 + protected $con;
  16 +
15 17 public function setUp()
16 18 {
17 19 parent::setUp();
@@ -20,6 +22,14 @@ public function setUp()
20 22 }
21 23 }
22 24
  25 + public function tearDown()
  26 + {
  27 + \Propel::enableInstancePooling();
  28 + if ($this->con) {
  29 + $this->con->useDebug(false);
  30 + }
  31 + }
  32 +
23 33 public function testParamConverterSupport()
24 34 {
25 35 $paramConverter = new PropelParamConverter();
@@ -155,27 +165,140 @@ public function testParamConverterFindWithOptionalParam()
155 165
156 166 public function testParamConverterFindWithMapping()
157 167 {
158   - $paramConverter = new PropelParamConverter();
159   - $request = new Request(array(), array(), array('toto' => 1, 'book' => null));
  168 + $paramConverter = new PropelParamConverter();
  169 + $request = new Request(array(), array(), array('toto' => 1, 'book' => null));
160 170 $configuration = new ParamConverter(array('class' => 'Propel\PropelBundle\Tests\Fixtures\Model\Book',
161 171 'name' => 'book',
162 172 'options' => array('mapping' => array('toto' => 'id'))
163   - ));
164   - $paramConverter->apply($request, $configuration);
165   - $this->assertInstanceOf('Propel\PropelBundle\Tests\Fixtures\Model\Book',$request->attributes->get('book'),
  173 + ));
  174 + $paramConverter->apply($request, $configuration);
  175 + $this->assertInstanceOf('Propel\PropelBundle\Tests\Fixtures\Model\Book',$request->attributes->get('book'),
166 176 'param "book" should be an instance of "Propel\PropelBundle\Tests\Fixtures\Model\Book"');
167 177 }
168 178
169   - public function testParamConverterFindSlugWithMapping()
170   - {
171   - $paramConverter = new PropelParamConverter();
172   - $request = new Request(array(), array(), array('slugParam_special' => 'my-book', 'book' => null));
  179 + public function testParamConverterFindSlugWithMapping()
  180 + {
  181 + $paramConverter = new PropelParamConverter();
  182 + $request = new Request(array(), array(), array('slugParam_special' => 'my-book', 'book' => null));
173 183 $configuration = new ParamConverter(array('class' => 'Propel\PropelBundle\Tests\Fixtures\Model\Book',
174 184 'name' => 'book',
175 185 'options' => array('mapping' => array('slugParam_special' => 'slug'))
176   - ));
177   - $paramConverter->apply($request, $configuration);
178   - $this->assertInstanceOf('Propel\PropelBundle\Tests\Fixtures\Model\Book',$request->attributes->get('book'),
179   - 'param "book" should be an instance of "Propel\PropelBundle\Tests\Fixtures\Model\Book"');
  186 + ));
  187 + $paramConverter->apply($request, $configuration);
  188 + $this->assertInstanceOf('Propel\PropelBundle\Tests\Fixtures\Model\Book',$request->attributes->get('book'),
  189 + 'param "book" should be an instance of "Propel\PropelBundle\Tests\Fixtures\Model\Book"');
  190 + }
  191 +
  192 + public function testParamConvertWithOptionWith()
  193 + {
  194 + $this->loadFixture();
  195 + \Propel::disableInstancePooling();
  196 +
  197 + $paramConverter = new PropelParamConverter();
  198 + $request = new Request(array(), array(), array('id' => 1, 'book' => null));
  199 + $configuration = new ParamConverter(array(
  200 + 'class' => 'Propel\PropelBundle\Tests\Request\ParamConverter\MyBook',
  201 + 'name' => 'book',
  202 + 'options' => array(
  203 + 'with' => 'MyAuthor'
  204 + )
  205 + ));
  206 +
  207 + $nb = $this->con->getQueryCount();
  208 + $paramConverter->apply($request, $configuration);
  209 +
  210 + $book = $request->attributes->get('book');
  211 + $this->assertInstanceOf('Propel\PropelBundle\Tests\Request\ParamConverter\MyBook', $book,
  212 + 'param "book" should be an instance of "Propel\PropelBundle\Tests\Request\ParamConverter\MyBook"');
  213 +
  214 + $this->assertEquals($nb +1, $this->con->getQueryCount(), 'only one query to get the book');
  215 +
  216 + $this->assertInstanceOf('Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor', $book->getMyAuthor(),
  217 + 'param "book" should be an instance of "Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor"');
  218 +
  219 + $this->assertEquals($nb +1, $this->con->getQueryCount(), 'no new query to get the author');
  220 + \Propel::enableInstancePooling();
  221 + }
  222 +
  223 + public function testParamConvertWithOptionWithLeftJoin()
  224 + {
  225 + $this->loadFixture();
  226 + \Propel::disableInstancePooling();
  227 +
  228 + $paramConverter = new PropelParamConverter();
  229 + $request = new Request(array(), array(), array('param1' => 10, 'author' => null));
  230 + $configuration = new ParamConverter(array(
  231 + 'class' => 'Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor',
  232 + 'name' => 'author',
  233 + 'options' => array(
  234 + 'with' => array(array('MyBook', 'left join')),
  235 + 'mapping' => array('param1' => 'id'),
  236 + )
  237 + ));
  238 +
  239 + $nb = $this->con->getQueryCount();
  240 + $paramConverter->apply($request, $configuration);
  241 +
  242 + $author = $request->attributes->get('author');
  243 + $this->assertInstanceOf('Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor', $author,
  244 + 'param "author" should be an instance of "Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor"');
  245 +
  246 + $this->assertEquals($nb +1, $this->con->getQueryCount(), 'only one query to get the book');
  247 +
  248 + $books = $author->getMyBooks();
  249 + $this->assertInstanceOf('PropelObjectCollection', $books);
  250 + $this->assertCount(2, $books, 'Author should have two books');
  251 +
  252 + $this->assertEquals($nb +1, $this->con->getQueryCount(), 'no new query to get the books');
  253 + }
  254 +
  255 + protected function loadFixture()
  256 + {
  257 + $this->loadPropelQuickBuilder();
  258 +
  259 + $schema = <<<XML
  260 +<database name="default" package="vendor.bundles.Propel.PropelBundle.Tests.Request.DataFixtures.Loader" namespace="Propel\PropelBundle\Tests\Request\ParamConverter" defaultIdMethod="native">
  261 + <table name="my_book">
  262 + <column name="id" type="integer" primaryKey="true" />
  263 + <column name="name" type="varchar" size="255" />
  264 + <column name="my_author_id" type="integer" />
  265 + <foreign-key foreignTable="my_author" onDelete="CASCADE" onUpdate="CASCADE">
  266 + <reference local="my_author_id" foreign="id" />
  267 + </foreign-key>
  268 + </table>
  269 +
  270 + <table name="my_author">
  271 + <column name="id" type="integer" primaryKey="true" />
  272 + <column name="name" type="varchar" size="255" />
  273 + </table>
  274 +
  275 +</database>
  276 +XML;
  277 + $builder = new \PropelQuickBuilder();
  278 + $builder->setSchema($schema);
  279 + if (class_exists('Propel\PropelBundle\Tests\Request\ParamConverter\MyAuthor')) {
  280 + $builder->setClassTargets(array());
  281 + }
  282 + $this->con = $builder->build();
  283 + $this->con->useDebug(true);
  284 +
  285 + MyBookQuery::create()->deleteAll($this->con);
  286 + MyAuthorQuery::create()->deleteAll($this->con);
  287 +
  288 + $author = new MyAuthor();
  289 + $author->setId(10);
  290 + $author->setName('Will');
  291 +
  292 + $book = new MyBook();
  293 + $book->setId(1);
  294 + $book->setName('PropelBook');
  295 + $book->setMyAuthor($author);
  296 +
  297 + $book2 = new MyBook();
  298 + $book2->setId(2);
  299 + $book2->setName('sf2lBook');
  300 + $book2->setMyAuthor($author);
  301 +
  302 + $author->save($this->con);
180 303 }
181 304 }

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.