The Ultimate ORM Query API. Only With Propel.

This post is pretty long, but it explains a new shift in the Propel Query API, and shows how this new syntax will be the killer feature of Propel 1.5. You'll soon learn more about it, but first, let's see is how the idea came.

Better Is Not Best

The latest additions in the Propel Query API intended to make it easier to read and write queries. For one part, an important factor of readability is the amount of code necessary to write a query. From this point of view, the recent ModelCriteria syntax, introduced in the 1.5 branch, appears as an important improvement:

// Propel 1.4 way
$c = new Criteria();
$c->add(BookPeer::PRICE, 40, Criteria::LESS_THAN);
$c->addAscendingOrderByColumn(BookPeer::TITLE);
$book = BookPeer::doSelectOne($c);
 
// Propel 1.5 way
$book = PropelQuery::from('Book')
  ->where('Book.Price < ?', 40)
  ->orderBy('Book.Title')
  ->findOne();

But after introducing this new syntax in a recent blog post, I received negative reactions from various Propel users who complained about the lost ability to use autocompletion in an IDE.

At first, I saw these complaints as reluctance to change. I thought that people would get used to the new syntax pretty quickly. They would also see how the added benefits of `ModelCriteria` compensate the lost IDE autocompletion.

IDE Autocompletion Is A Key Feature

And then I tried it myself, with an IDE. I have to admit that autocompletion helps a lot. In fact, using NetBeans, the Propel 1.5 syntax requires a little longer to write than the Propel 1.4 syntax. Besides, the IDE provides the comfort and insurance that the code was correct before running it, while any typo in the 1.5 code can only appear at runtime.

That didn't decide me to use an IDE for day-to-day coding - I'm satisfied with the reactivity of Textmate and I don't like the blinking interfaces of IDEs. But that opened my eyes on a key feature that Propel has, and that no other ORM offers. The ability to use autocompletion is one big advantage of Propel that can't be left aside, or else Propel will lose part of its IDE-users community.

That didn't decide me to drop the Propel 1.5 syntax either, because after years of using Propel, I can't read a Criteria query without whispering, and I can't write one without complaining. The fast syntax offered by other ORMs makes Propel look like C. The Propel 1.4 syntax is still counterintuitive, and the fact that it relies on class constants makes a lot of thinks impossible.

That means that the ModelCriteria syntax is just a step in the path of The Definitive Query Syntax. But then how to improve it?

Code Generation To The Rescue

ModelCriteria uses runtime introspection to offer very powerful features. And it also uses code generation to support query behaviors. In fact, any call to `PropelQuery::from('Book')` returns an instance of `BookQuery`, which is a class generated by Propel alongside `Book` and `BookPeer`.

But the `PropelQuery::from()` factory has no way to tell an IDE which class it is going to return. The phpDoc for this method stipulates that the return value is an instance of `ModelCriteria`, but in reality it's a subclass of it.

So the first step is to improve the generated `BookQuery` to give it the charge of the factory. By generating a `BookQuery::create()` method, returning a new instance of `BookQuery`, it becomes possible to tell the IDE which class is actually in use:

 // Old Propel 1.5 way
 $book = PropelQuery::from('Book')
   ->where('Book.Price < ?', 40)
   ->orderBy('Book.Title')
   ->findOne();
 
// New Propel 1.5 way
 $book = BookQuery::create()
   ->where('Book.Price < ?', 40)
   ->orderBy('Book.Title')
   ->findOne();

Generated Filter Methods

But the generated query objects, like `BookQuery`, have very few methods. And there is no way an IDE will help the developer to write the content of a string, as in the `where()` argument. So it's necessary to modify the generator of the custom Query objects again, in order to add one `where()` method for every column of the table. And since the operator cannot be part of the method name, it has to move as second argument of the call. The example query then becomes:

class BookQuery extends BaseBookQuery
{
  // this method is generated

  /**
   * Filter the query on the price column
   * 
   * @param     mixed $price The value to use as filter.
    * @param     string $operator Operator to use for the comparison
   *
   * @return    BookQuery The current query, for fluid interface
   */
  public function wherePrice($price, $operator = Criteria::EQUAL)
   {
    return $this->whereColumn('Price', $price, $operator);
  }
}

 // New Propel 1.5 way
 $book = BookQuery::create()
   ->wherePrice(40, '<=')
   ->orderBy('Book.Price')
   ->findOne();
 

Notice how the phpDoc of the generated `wherePrice()` method stipulates that the method returns a `BookQuery` and not only a `ModelCriteria`. This is important to allow IDEs to keep on suggesting the correct methods when the fluid interface is used.

After seing the `wherePrice()` method call, you probably agree that it is not very good looking ; the operator without the column name is a call for a return to the Criteria constants. Maybe there is an alternative solution. Since the `wherePrice()` method is generated, the generator can use the fact that the `price` column is numeric to offer additional abilities. After further thinking, each column type should offer special comparison types : a string column should support wildcards, a boolean column should support string booleans (like 'no' or 'false'), etc.

So after a new addition to the query generator, here is how the query looks:

class BookQuery extends BaseBookQuery
 {
  // this method is generated
 
  /**
   * Filter the query on the price column
   * 
   * @param     double|array $price The value to use as filter.
   *            Accepts an associative array('min' => $minValue, 'max' => $maxValue)
    *
   * @return    BookQuery The current query, for fluid interface
   */
  public function wherePrice($price = null)
  {
    if (is_array($price)) {
      if (isset($price['min'])) {
        $this->whereColumn('Price', $price['min'], Criteria::GREATER_EQUAL);
       }
      if (isset($price['max'])) {
        $this->whereColumn('Price', $price['max'], Criteria::LESS_EQUAL);
      }
    } else {
      return $this->whereColumn('Price', $price, Criteria::EQUAL);
     }
  }
}

 // New Propel 1.5 way
 $book = BookQuery::create()
   ->wherePrice(array('max' => 40))
   ->orderBy('Book.Title')
   ->findOne();
 

The final touch is to rename these `whereXXX()` methods into `filterByXXX()` (and to rename `whereColumn()` to `filterBy()`). It's not just a matter of taste, it's a similarity with `orderBy()` and `groupBy()`, and a deliberate step away from relational SQL. More on that matter later, but in the meantime, here is the query again:

class BookQuery extends BaseBookQuery
 {
  // this method is generated

  /**
   * Filter the query on the price column
   * 
   * @param     double|array $price The value to use as filter.
   *            Accepts an associative array('min' => $minValue, 'max' => $maxValue)
    *
   * @return    BookQuery The current query, for fluid interface
   */
   public function filterByPrice($price = null)
  {
    if (is_array($price)) {
      if (isset($price['min'])) {
        $this->filterBy('Price', $price['min'], Criteria::GREATER_EQUAL);
       }
      if (isset($price['max'])) {
        $this->filterBy('Price', $price['max'], Criteria::LESS_EQUAL);
      }
    } else {
      return $this->filterBy('Price', $price, Criteria::EQUAL);
     }
  }
}

 // New Propel 1.5 way
 $book = BookQuery::create()
   ->filterByPrice(array('max' => 40))
   ->orderBy('Book.Price')
   ->findOne();
 

phpDoc For Magic Methods

As for allowing the `orderBy()` method to use IDE completion, using generated methods is out of the question. It is acceptable to add filter methods that react differently according to the column type, it is not acceptable to add one-line proxy methods for each column just to support an `orderByTitle()` syntax.

The ModelCriteria already supports a call to `orderByTitle()`, through the magic `__call()` method. But magic and IDE completion don't get along well with each other. Or do they?

The phpDocumentor grammar allows one to document magic methods by way of `@method` comments in the class description, that the IDEs recognize. So the alternative to generating one method for every column is to generate one comment line for every column. That way the IDE sees the `orderByTitle()` method at development time, but this method doesn't bloat the `BookQuery` code since `__call()` already handles it pretty well.

So the query can be even more 'IDEified' as follows:

 /**
 * Base class that represents a query for the 'book' table.
 *
 * Book Table
 *
 * @method     BookQuery orderById($order = Criteria::ASC) Order by the id column
 * @method     BookQuery orderByTitle($order = Criteria::ASC) Order by the title column
  * @method     BookQuery orderByISBN($order = Criteria::ASC) Order by the isbn column
 * @method     BookQuery orderByPrice($order = Criteria::ASC) Order by the price column
 * @method     BookQuery orderByPublisherId($order = Criteria::ASC) Order by the publisher_id column
  * @method     BookQuery orderByAuthorId($order = Criteria::ASC) Order by the author_id column
 */
class BookQuery extends BaseBookQuery
 {
   ...
 }
 
 // New Propel 1.5 way
 $book = BookQuery::create()
   ->filterByPrice(array('min' => 40))
   ->orderByTitle()
   ->findOne();
 

The same addition is required for `groupByXXX()` methods.

And while adding smart phpDoc comments to the query class description, why not override the phpDoc for `findOne()`? That way an IDE knows that the returned `$book` is not just a Propel `BaseObject`, it's a `Book`.

How About Relationships?

So far, so good, but how about the handling of conditions on related tables? For instance, how to translate this Propel 1.4 query into the new syntax?

 // Find first book published at 'Penguin' editions
 // Propel 1.4 way
$c = new Criteria();
$c->addJoin(BookPeer::PUBLISHER_ID, PublisherPeer::ID);
$c->add(PublisherPeer::NAME, 'Penguin');
$book = BookPeer::doSelectOne($c);

This is in fact quite easy, since a recent addition to the `ModelCriteria` class allows it to use a secondary query object instead of applying conditions on a related column. So without any change in the Query generator, this can be written as follows:

// New Propel 1.5 way
 $book = BookQuery::create()
   ->join('Book.Publisher')
   ->useQuery('Publisher')  // returns a new PublisherQuery instance
    ->filterByName('Penguin')
  ->endUse()       // returns the original BookQuery instance, merged with the PublisherQuery
   ->findOne();
 

But the two calls to `join()` and `useQuery()` are not very IDE friendly. No problem, let's modify the query class generator again, so as to add support for a faster `use()` syntax:

 class BookQuery extends BaseBookQuery
{
  // this method is generated

  /**
   * Use the Publisher relation Publisher object
   *
   * @return    PublisherQuery A secondary query class using the current class as primary query
    */
  public function usePublisherQuery()
  {
    return $this
      ->join($this->getModelAliasOrName() . '.Publisher')
      ->useQuery('Publisher', 'PublisherQuery');
   }
}

// New Propel 1.5 way
 $book = BookQuery::create()
   ->usePublisherQuery()  // returns a new PublisherQuery instance and makes the join
    ->filterByName('Penguin')
  ->endUse()       // returns the original BookQuery instance, merged with the PublisherQuery
   ->findOne();
 

This is getting very interesting. Since this method is created at generation time, it's possible to create `useXXXQuery()` for every foreign key, but also for the foreign keys of other tables pointing to `Book`.

Syntactic Sugar

And the subject of conditions on related tables is not closed. Propel still requires that you know foreign key columns to apply a condition on a related object. For instance:

 // Propel 1.4 way
// $author is an Author object
$c = new Criteria();
$c->add(BookPeer::AUTHOR_ID, $author->getId());
$book = BookPeer::doSelectOne($c);

// new Propel 1.5 way
$book = BookQuery::create()
   ->filterByAuthorId($author->getId())
  ->findOne();

An ORM is all about thinking "objects" rather than "relational", and yet this kind of query forces the developer to remember the relation between the columns of two tables. So in the process of adding generated methods to the Query classes, let's make a tiny but very useful addition:

 class BookQuery extends BaseBookQuery
 {
   // this method is generated
 
   /**
   * Filter the query by a related Author object
   *
   * @param     Author $author the related object to use as filter
   * @return    BookQuery The current query, for fluid interface
   */
  public function filterByAuthor(Author $author)
   {
    return $this
      ->filterBy('AuthorId', $author->getId());
  }
 }
 
 $book = BookQuery::create()
   ->filterByAuthor($author)
   ->findOne();
 

Wrapping It All Together

The generated query classes now have much more methods and phpDocumentation, and in fact everything is ready to provide a full IDE completion support. Let's check on a final example:

// $author is an Author object
// Find all books by $author, sold for less than 40$, ordered by title, and published at 'Penguin' editions

// Version A: Propel 1.4 way
 $c = new Criteria();
$c->add(BookPeer::AUTHOR_ID, $author->getId());
$c->add(BookPeer::PRICE, 40, Criteria::LESS_THAN);
$c->addAscendingOrderByColumn(BookPeer::TITLE);
$c->addJoin(BookPeer::PUBLISHER_ID, PublisherPeer::ID);
 $c->add(PublisherPeer::NAME, 'Penguin');
$book = BookPeer::doSelectOne($c);

// Version B: Old Propel 1.5 way


$book = PropelQuery::from('Book')
  ->where('Book.AuthorId = ?', $author->getId())
  ->where('Book.Price < ?', 40)
  ->orderBy('Book.Title')
  ->join('Book.Publisher')
  ->where('Publisher.Name = ?', 'Penguin')
  ->findOne();

// Version C: New Propel 1.5 way
$book = BookQuery::create()
  ->filterByAuthor($author)
  ->filterByPrice(array('min' => 40))
  ->orderByTitle()
  ->usePublisherQuery()
     ->filterByName('Penguin')
  ->endUse()
  ->findOne();

Version C is both more concise than versions A and B, and totally compatible with IDE completion. It is still very readable, less error prone than version B, and extremely easy to extend.

The Meaning Of Everything

Incidentally, the new syntax cuts the link with the SQL query. The syntax doesn't show any `where()` or `join()` calls. This is because the Propel Query API is now truly in the object oriented world.

Also, this new Propel Query syntax uses code generation the same way the Propel Models do. Code generation provides intuitive and IDE friendly methods. And these generated methods are very fast, because they don't rely on string parsing or model introspection. Version C is faster to execute than version B.

Speed and code completion are the marks of Propel. No other ORM offer such good development tools that are also very performant at runtime. That's the sign that this new syntax is the ultimate query syntax for Propel.

One last thing: if version C will be the default syntax for queries in Propel 1.5, versions A and B will also be supported. That means no backwards compatibility problem, and no switching problems for Doctrine users who might want to try the mighty Propel Query API.

Posted by Francois Zaninotto 

40 comments

Jan 06, 2010
Thibault said...
Hands up! I really love your work and your thoughts, you always bring PHP closer to excellence.
And this time you made even better than with DbFinder...
Propel is a great project, I'm glad to see it's more than ever in good hands.

Keep up the good work!

Cheers,
Thibault

Jan 06, 2010
annis said...
This just keeps getting better and better! Thank you!

Daniel

Jan 06, 2010
Christoph Schaefer said...
Fantastic,
you made it!
The API ported from DbFinder was already nice but I felt like there are still things missing to match Propels abilities provided by code generation.
This new API looks like the ultimate answer!

One suggestion:
I'm not sure if IDEs support phpDoc return type overwrite by derived classes?
If they do you could also generate phpDoc for the inherited methods of ModelCriteria to return the proper ModelQuery.

Thank you!
Chris

Jan 07, 2010
davide said...
Can I ask you something?
Why the column names are sort of camel-cased in the queries? I mean, it's just a matter of taste or there is a reason? Same for the table name, but I think that is to clarify that we are talking about the "Book" object and not the "book" table, right?

I didn't check the code, and this is totally just out of curiosity :)

Jan 07, 2010
@davide: Right. The class and column names are always camelcased in Propel code. So you know when you deal with tables, and when you deal with objects.

As a side note, Propel 1.4 offers two ways to refer to a column: BookPeer::PUBLISHER_ID, and $book->getPublisherId(). Propel 1.5 removes the need for the first syntax, so you are now sure that the right syntax for php code is Camelcase.
Jan 07, 2010
This is great !
Can i say i love You ? :)
This new syntax with this order and filter thing are great, it will Boost speed of code writing with power of Super Cow.
Are these changes available somewhere to test it ?
Jan 07, 2010
@Tomasz: Yes, checkout the 1.5 branch in the Propel suvbversion, rebuild your model, and you're ready to test the Super Cow Query API.
Jan 07, 2010
Nikolai said...
Looks nice

Just a question: why do you think about removing Peer classes after 1.x? Yes, if all use the new syntax, the underlying code can be refactored for 2.x. But until now this was the place, where I split business logic from its database contents.

If I have ten places where I need the books of the actual year then I create a method for it in the BookPeer which returns the collection.
Writing the query X times may be error-prone. Where we have to place such methods without Peer classes? Or must we create our own classes (what a pity :D)?

regards

Jan 07, 2010
I wonder what will be with custom equal. For example i'm using bits on integer column and sometime i must search for specific values
i can't make such query
SELECT * FROM table1 WHERE bits & 12 //4+8
because when a row have value 4 or 8 it will be selected and i must make
SELECT * FROM table1 WHERE bits & 12 = 12
With old criteria i must make Custom equal how it will be made here ?

@Nikolai I think You will be able to make Your own methods in BooksQuery Class i think ObjectQuery will replace all functionality of ObjectPeer. Of course if they will be extended in the same way

Jan 07, 2010
Nikolai said...
@Tomasz Budzyński: I said nothing if this is the case ;) I only read this blog and don´t checked the SVN
Jan 07, 2010
@Nikolai: Yes, the right place to put you custom query logic will be the query classes rather than the Peer classes in Propel 1.5. And yes, the Peer classes serve no other purpose than backwards compatibility, so they may disappear in the future. But I'm not in charge of the Propel 2.0 branch...
Jan 07, 2010
@Tomasz: The generated Query classes still inherit ModelCriteria, so you can easily do:

$ts = Table1Query::create()->where('Table1.bits & ?', 12)->find();
Jan 07, 2010
Brendon said...
really great, we just updated to 1.4 and now can't wait for 1.5
Jan 07, 2010
lvanderree said...
Nice job!

Lately I was looking at Doctrine, since community support was getting a little low for Propel and I didn't had the time/opportunity to improve things myself. However since your work at Propel, and especially this addition, I think many people will consider using Propel (again) for future projects.

At least I do!

Jan 08, 2010
Mike said...
Wow, this is really great for a lot of queries. Where I am having issues is with joins, as the Use*** functions break code completion (the IDE sees a ModelCriteria object returned and can no longer hint the ModelQuery class-specific methods). Same thing happens if you need to mix in joinWith() to hydrate related objects. Any ideas?
Jan 08, 2010
Christoph Schaefer said...
As I already suggested above, this issue maybe fixed by phpDoc return type override (the same way its already done for findOne).
Not sure though, if Francoise already faced this problem...
Jan 08, 2010
Christoph Schaefer said...
-e
Jan 09, 2010
Nick said...
Amazing work.

A couple of typos in your code snippets: 3rd code snippet uses both $price and $value (should only be one), and 4th code snippet uses whereByPrice() and wherePrice() (again, should only be one).

Regarding this syntax:

->filterByPrice(array('min' => 40))

When I first looked at that, it seems to be telling me "where the minimum price is 40", or in other words, "WHERE price >= 40", which is exactly the opposite of what it really means.

Is this "min" syntax a standard taken from somewhere else, or has it been created purely for Propel 1.5? It seems kind of counter-intuitive to me.

In fact, I just had a look at the code inside that method, and it looks like it is indeed meant to be used as I described above, and the examples in your post should be using "max" instead of "min", yes? Actually even "max" is not right, since it includes Books that have a price equal to $40, but Versions A and B just query for < $40. I'm sure this will tidied up when Propel 1.5 is released though. ;)

Jan 09, 2010
@Nick: Thanks for proofreading the post so carefully. These were all typos that I've just fixed. It proves that a second eye always spots new problems :)
Jan 09, 2010

@Mike: The useXXXQuery() methods return the correct phpDoc declaration for return type. Maybe you're referring to the return type declared by endUse()? In that case, there's nothing I can do - except catching that a call to useXXXQuery() on a secondary query built from QueryXXX returns the original XXXQuery, instead of a new one. Does it make sense?

[code lang='php']
$book = BookQuery::create()
  ->usePublisherQuery()  // returns a new PublisherQuery instance and makes the join 
    ->filterByName('Penguin')
  ->useBookQuery()       // returns the original BookQuery instance, merged with the PublisherQuery
  ->findOne();

[/code]
Jan 09, 2010
lvanderree said...
Hi François, I think this can introduce some problems where you have a (circular) where XXX relates to YYY and it in its turn again relates to XXX. For example a City is related to a Country and from this Country I again want to get the Capital City. This structure would get impossible to query when the useCityQuery would return the CityQuery-reference without the join between country and capital-city, wouldn't it?

Maybe a solution is to define magic alternatives for endUse(), like returnToCity()?

Jan 09, 2010
Mike said...
@Francois: Yes, that makes sense as a way to recover the original query type. Maybe useXXXQuery() methods should be generated for all the reverse one to many relationships (alternatively these could be named returnXXXQuery(), as suggested above):
[code lang='php']
$book = BookQuery::create()
->usePublisherQuery() // returns a new PublisherQuery instance and makes the join
->useLocationQuery()
->filterByCity('New York')
->usePublisherQuery() // returns PublisherQuery merged w/ LocationQuery
->useBookQuery() // returns BookQuery merged w/ PublisherQuery
->findOne();
[/code]

Also, do you plan to support arbitrary hydration of joined objects in the new syntax?

Jan 09, 2010
xplo said...
Hi,
i m happy that you see autocompletion as a key feature :)

I tried it with sfPropel15Plugin and i can see all the new method however i cannot find any create method on the Query Table Class

Jan 10, 2010
@mike: I think that lvanderreee's suggestion of generated returnToXXXQuery() @method tags, that simply proxy to endUse(), is the best tradeoff in terms of performance and functionality.

Also, arbitrary hydration of joined objects is in Propel 1.5 since a long time... It's called 'with()' (see http://propel.phpdb.org/trac/wiki/Users/Documentation/1.5/ModelCriteria#MinimizingQueries)

Jan 10, 2010
@xplo: create() is generated in the stub query class (the one where you add your own methods) and not in the base query class. If you used Propel 1.5 in the past, the Propel generator won't rebuild these classes because they are present. That means that you have to delete your XXXQuery classes so that Propel re-generates them.

That's what it takes to be on the cutting edge!

Jan 10, 2010
Mike said...
Yes, I'm aware that I can use with() or joinWith(), but was wondering if you were planning on generated methods in the query classes that replicated this functionality without having to use literals for the FK relations.
Jan 10, 2010
xplo said...
Thx you Francois that was it, i needed to delete the query class ~

Mike i d like that too but i dont see how the join could add the getTableObject method dynamicly, it it even possible in php

Jan 11, 2010
istaveren said...
Look very clean the new propel query language.
The IDE support and the runtime speed are the most important things that I was missing in Doctrine.

Thanks for the good work :-)

Jan 11, 2010
Andy Young said...
Looks extremely nice Francois. Thanks again for putting so much effort in.

The one bit that still bugs me is the use....()....endUse() syntax. Would it be possible to improve the syntax to resemble the following?

// Version D: New New Propel 1.5 way..?
$book = BookQuery::create()
->filterByAuthor($author)
->filterByPrice(array('min' => 40))
->orderByTitle()
->use(PublisherQuery()::create()->filterByName('Penguin'))
->findOne();

(I haven't had the chance to understand yet whether PublisherQuery is the relevant object type - if incorrect replace with a different class invented for this purpose, it's the syntax I'm talking about)

This would make it easier to work with queries pragmatically too.

In the ideal case it would support repeated calls to use() with the same relational Query type e.g:

$book = BookQuery::create()
->use(PublisherQuery()::create()->filterByName('Penguin'))
->use(PublisherQuery()::create()->filterByLocation('New York'))
->findOne();

While the above looks silly since it can obviously be shortened, the thinking is again to support manipulating these queries at run-time.

Best,
-- A

Jan 11, 2010
@Andy: The problem is that the embedded query must know that it's embedded right from the beginning, because is must be able to use a true table alias. With your solution, the embedded query is executed in a standalone mode before being embedded, so it's not possible with the current implementation.

As for repeated use of related queries, which query do you expect with that example? Should there be one or two joins on the publisher table? It's not very obvious.

Also, could you provide me an example of use of your syntax where it would be much better than the current one? I fail to see any use case where it would bring a significant increase in usability...
Jan 12, 2010
lvanderree said...
Hi François, I have started reading the 1.5 documentation, and for now only found a minor difference in the values from the examples and the sql-previews. Like in:

<?php
$c = new Criteria();
$c->addCond('cond1', BookPeer::TITLE, 'Foo', Criteria::EQUAL); // creates a condition named 'cond1'
$c->addCond('cond2', BookPeer::TITLE, 'Bar', Criteria::EQUAL); // creates a condition named 'cond2'
$c->combine(array('cond1', 'cond2'), Criteria::LOGICAL_OR); // combine 'cond1' and 'cond2' with a logical OR
// translates in SQL as
// WHERE (book.TITLE = 'foo' OR book.TITLE = 'bar')
?>

Where in the php-code "Foo" and "Bar" are written with a Captial, while in the sql-example "foo" and "bar" are used. You do this pretty often, so maybe intentionally but I can't see why...

Besides this I could not yet find anything wrong.

Jan 12, 2010
@lvanderree: Thanks for the feedback. Could you send this to the developers mailing-list, or open a new ticket about the problems you spot? This thread of comments is getting harder to read...
Jan 12, 2010
Andy Young said...
@Francois Thanks for the quick reply - let me explain a bit more.

There's two considerations: (A) - use cases / reasons it's better & (B) - how to implement.

(A) Use case: We do a lot of manipulating Criteria using functions. This is great for abstracting generic parts of query construction. E.g.:

function addGenericStuffToCriteria(Criteria $c) {
$c->doStuff..
}
function modifyCriteriaUsingPattern(Criteria $c, $valueA, $valueB) {
$c->doStuff..($valueA);
$c->otherStuffWith..($valueB);
}

and we have Criteria "factories" e.g.

function generateCriteriaForSomething() {
$c = new Criteria();
$c->doStuff..
return $c;
}

$c = generateCriteriaForSomething();
// add last-minute customisations for this specific use
$c->doStuff..

Because of the genius move of making the new PropelQuery stuff extend ModelCriteria which extends the original Criteria, this is still all possible using 100% the cool new syntax..until you try and work with filters on the query that affect related objects - i.e. the use...() stuff. Basically I'm talking about the ability to do anything you want with a query object and obtain predictable results without knowing what's been done with it before, as you can with the current Criteria.

Hopefully you can see now how the patterns above are equivalent to the repeated use of related queries I gave before. To clarify, there should be just the single join produced by that example.

Other reasons why it's better syntax:

- it cuts out a method call - endUse() - and with it the scope for bugs when we forget to include it / make mistakes during repeated editing of complex queries etc.

- it solves the problem of "losing" the type of the original Query object for autocompletion etc, since FooQuery::useBlahQuery() will always return FooQuery just like everything else.

- it's more "object orientated" style, rather than the procedural "BEGIN..END" pattern.. (if that's even a reason! ;) This is the theoretical basis for the additional flexibility it gives, though)

ok, now (B) - how to implement. I realised it wasn't possible to use that exact code with the current implementation as it stands, that's why I suggested you imagine a different class if relevant.

If you agree that it's improved syntax, you could do something like the following to implement:

- have the "parent" query notify the "child" query that it's embedded during the use() method so that it can use a suitable table alias. make sure that it doesn't need to know this right from the beginning - I assume everything's stored as metadata inside the object until it needs to be executed, just like with the current Criteria - so that shouldn't be too much of a problem?

- if the above is a problem, what would be required is a lightweight metadata class rather than the full query object that can hold the subquery data until the "parent" query requires it, during the final execution of the query.

- have the "parent" query store a list of all it's "child" queries, indexed by object type so they can be combined in the relevant way if passed more than once for the same object.

Any more clarifications required, let me know.

Thanks,
-- A

Jan 12, 2010
@Andy: I see another problem with embedding a query like that: the main query doesn't know how it is related to it. It must inspect the query class to guess the relation. Even worse: if there are two possible relations on the same table, your use() method has no way to know which relation to use for the join. In the generated useXXXQuery(), 'XXX' is a Relation name, not a Model name. That makes a huge difference.

Anyway, I like your syntax better than the 'procedural' one, and I think we should go on looking for a solution. But I suggest that we continue the discussion on the developers mailing-list, so that other devs gave give their opinion on the matter.
Jan 13, 2010
Andy Young said...
cool - i'll have a think about these issues and post to the dev list.
Jan 16, 2010
Ventzy said...
I can't imagine programming without IDE and autocomplete, maybe because I am coming from Delphi, which is powerful IDE. I am not remembering-and-typing machine, so I am very happy with your efforts, Propel to remain as IDE-friendly as possible.
Jan 19, 2010
Crafty_Shadow said...
Just a little note,
For the last examples, C should be like:
...
# ->filterByPrice(array('min' => 39))
...
Because the condition in A is Criteria::LESS_THAN, and filterByXXX uses Criteria::LESS_EQUAL
Jan 22, 2010
Benjamn Runnels said...
I'm going through now an upgrading my DbFinder usage to Propel 1.5 and the new autocomplete is a treat. For people going from DbFinder to Propel 1.5 it will be a very simple process.

Thanks a lot Francois. I loved DbFinder but the new autocomplete abilities make Propel 1.5 so much nicer.

Feb 03, 2010
Ralph said...
Hi Francois,

This is an excellent demonstration of Documentation Driven Development!

It filled my heart with yonder!

Great job on propel! Just in time to make me decide to stick with it :)

Kind regards,

Ralph

Nov 16, 2010
Raziel said...
Hi francois.
Please I need some help.
I tried to do something like that wit propel.
select distinct b.parameter_id, b.* from table1 a inner join table2 As b on a.parameter_id = 5 and a.pk_table1 = b.fk_table2 inner join table3 as c ON b.some_parameter_id = 1 and b.pk_table2 = c.fk_table3.

And i found the following problems.

first: can't format the select statement
->select(array('b.ParameterId', 'b.*')) // here b.* not works
How can I do this?

second:
when joined tables propel add the conditions the filterBy and where statement are push to the end of a query in where clause; How I can add a condition filter in the join it self to concatenate filter and where.

I hope you understand my english y you will can answer to me.
Thaks
Best Regards.

Leave a comment...