What I am trying to do is write a "search" class that can search for a list of products and store them in an array.
I already have a "product" class that can be used to get the details of a specific product.
Here is my code:
class Product {
public $name;
public $price;
public $description;
public function getProductById ($id) {
$sql = 'SELECT name, price, description FROM product WHERE id = ' . $id;
$row = /* MySQL functions here to execute SQL statement and get a matching row */
$this->name = $row['name'];
$this->price = $row['price'];
$this->description = $row['description'];
return TRUE;
}
}
class Search {
public $results;
public $totalResults;
function __construct() {
$this->results = array ();
$this->totalResults = 0;
}
public function doSearch ($name) {
$sql = 'SELECT id FROM product WHERE name LIKE "%' . $name . '%"';
$rows = /* MySQL functions here to execute SQL statement and get a list of matching product ID's */
foreach ($rows as $row) {
$product = new Product;
$product->getProductById ($row['productid']);
$this->results[] = $product;
}
return TRUE;
}
}
$search = new Search;
$search->doSearch ('Fresh Flowers');
The problem with the above is that every matching record in the doSearch method will execute a query in the getProductById method. If there are 100 matching products, there will be 100 individual queries carried out in the Product class.
However, if I get the products directly in the doSearch method using a single query, this will then bypass the Product class altogether.
When a "product" is an object, what's the most appropriate way to write a search class that can return a list of "product" objects without the overhead of what I'm doing above?
Add a constructor to the Product class which takes name, price and description as parameters (or an assoziative array), to populate the object with the necessary values, decoupled of the database query.
Within doSearch, you can then create a SELECT which not only gets the ID but all relevant fields from the products table, and create the populated product objects immediately with new Product($name, $price, $description) or new Product($row), without calling getProductById for each product.
Create a class that populates instances of Product with data from the database.
The class can then create one or multiple instances of the Product class depending on how much data is being fetched.
Conclusion: Extract the getProductById from your Product class and put it somewhere else. It is a specialised method that only populates one instance.
Just grab what you want in the first place.
public function doSearch ($name) {
$sql = 'SELECT id, name, price, description FROM product
WHERE name LIKE "%' . $name . '%"';
// now just return the array
}
Or use PDO to return result sets as objects.
$result->setFetchMode(PDO::FETCH_INTO, new animals);
as discussed here: How can I simply return objects in PDO?
Your Product class shouldn't know anything about a database. It should contain the values representing a product, nothing more. Extract all stuff dealing with the database out of this class.
Searching for products is one way to access a list of products. Your next class should be a list of products then. Only when accessing one single product you'd not have to deal with a list, but this is probably less often than you think.
Ok, you have the product and the list of products, you now can go one step forward and add database access. You need a class that deals with giving you both one product (when searching by id) and lists of products (when searching by some text or other stuff). Only this class allows you to deal with the queries needed to access the database. The result sets of each query may directly be used inside the "list of products" class, probably by inheriting all the stuff that is defined in the general "list of products" class and adding dealing with database results.
So in the end you'll end up having:
Product -> ListOfProducts -> ProductDatabase -> DatabaseAccessLayer
Related
I'm working on a site to store series.
My database contains a Programs table and a Category table.
The category table has an ID and a name.
The Programs table contains all the information about this program and the category id associated with it.
I have a /category/$categoryName function.
I would like, via the user-specified $categoryName, to display all programs that are in this category.
public function category(string $categoryName):Response{
$categoryInfos = $this->getDoctrine()
->getRepository(Category::class)
->findOneByName($categoryName);
$categoryID = $categoryInfos['id'];
gives me ->
"Cannot use object of type App\Entity\Category as array"
Once I have the category ID, I can display all the programs that belong to that category but
1) I can't retrieve the ID
2) I'm wondering if there isn't a simpler command to do what I'm trying to do.
Your result is an array so for accessing the id, first you need to access to first element of the array then you can access the id:
$categoryID = $categoryInfos[0]['id'];
if you know that you will have an unique result so you can use setMaxResult:
$categoryInfos = $this->getDoctrine()
->getRepository(Category::class)
->findByName($categoryName)
->setMaxResult(1);
and the result must be like:
object(App\Entity\Category)#501 (2) { ["id":"App\Entity\Category":private]=> int(1) ["name":"App\Entity\Category":private]=> string(7) "Horreur" }
finally you can use your own code to access the id:
$categoryID = $categoryInfos['id'];
if you still can not access to id, maybe the id is a private property and you can use getter functions, for accessing to the id, use getId() or for name use getName() and etc.
$categoryID = $categoryInfos->getId();
/**
* #Route("/category/{categoryName}", name="category")
*/
public function category(string $categoryName): Response
{
$categoryInfos = $this->getDoctrine()
->getRepository(Category::class)
->findOneByName($categoryName);
$categoryInfos->getId();
return $this->render('home.html.twig');
}
Well, that's it.
How does one get the parent category ID of a category ID in Magento 2?
In Magento 1, I did it with the following:
$product_id = 101; //for example
$product = Mage::getModel('catalog/product')->load($product_id); //get product object by product ID
$category_ids = $product->getCategoryIds(); //array of all categories that the product is in
foreach ($category_ids as $cat_ids) {
$parent_id = Mage::getModel('catalog/category')->load($cat_id)->getParentId(); //
echo $parent_id; //outputs an int ID of parent category
}
In Magento 2, I've been attempting the same with the following:
$product_id = 101; //again, for example
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$productRepository = $objectManager->create('\Magento\Catalog\Model\ProductRepository');
$product = $productRepository->getById($product_id); //get product object by product ID
$category_ids = $product->getCategoryIds(); //array of all categories that the product is in
foreach ($category_ids as $cat_ids) {
echo $cat_ids;
}
Up to here, my code is working perfectly and the $category_ids is an array of all the categories that the product is in. However I cannot figure out how to get the parent category IDs of each child category ID in the $category_ids array.
NOTICE* I'm aware that I'm not officially supposed to directly used the ObjectManager, so please save this from your answer. I am seeking to specifically use the ObjectManager in this manner to iterate over $category_ids and load the parent category IDs for each child category ID.
Like so often, there are multiple ways to achieve this.
The CategoryFactory route
To load a category directly, you load it via the Factory Singleton responsible for the \Magento\Catalog\Model\Category class. This is the \Magento\Catalog\Model\CategoryFactory class. From each instance of Category, you can simple call the method getParentId() to get the parent ID.
foreach ($categoryIds as $categoryId) {
try {
$category = $this->_categoryFactory->create()->load($categoryId);
} catch (\Exception $e) {
/* Handle this appropriately... */
}
echo 'Parent Category ID: ', $category->getParentId(), PHP_EOL;
}
In this example, $categoryIds is the array of Category IDs you extracted from your \Magento\Catalog\Model\Product instance.
The CategoryRepository route
Or preferably you can use a Singleton instance of the \Magento\Catalog\Model\CategoryRepository class as a wrapper around the Factory. It will handle all the loading with some added error handling and it will also store a reference to the loaded category for later reuse. So if you are doing this multiple times during one execution, or suspect that you will load the same category later on, using the Repository will optimize your performance.
foreach ($categoryIds as $categoryId) {
try {
$category = $this->_categoryRepository->get($categoryId);
} catch (\Exception $e) {
/* Handle this appropriately... */
}
echo 'Parent Category ID: ', $category->getParentId(), PHP_EOL;
}
The Collection route
This should be a much faster route, as you (1) load all categories once from database instead of using several multiple sql calls in the backend and (2) you have some control over what is populated in the Category, and what is left out. Please be aware, that pretty much only what you put in addAttributeToSelect() will be populated in the Collection. But if you're only after the parent_id this should not be an issue.
First, make sure you are familiar with collections, then acquire a CollectionFactory Singleton for Magento\Catalog\Model\ResourceModel\Category\CollectionFactory and then populate it like so:
/** #var \Magento\Catalog\Model\ResourceModel\Category */
$collection = $this->_categoryCollectionFactory->create();
# Specifically select the parent_id attribute
$collection->addAttributeToSelect('parent_id');
# Only select categories with certain entity_ids (category ids)
$collection->addFieldToFilter('entity_id', ['in' => $categoryIds])
# Iterate over results and print them out!
foreach ($collection as $category) {
echo 'Parent Category ID: ', $category->getParentId(), PHP_EOL;
}
With great powers comes great risk, however. This above code will have no error correction whatsoever. If there is a logical database error, such as a product which points to a missing category, this category will just be omitted from the collection and it will be up to you as a programmer to spot that and deal with it. Also, you will have to decide for yourself on how you are handling store view and active/inactive categories via filters to the collection.
The Direct Database route
Ok, I would not recommend this route unless you know exactly what you are doing, and are in desperate need for performance.
This will be crazy-fast, but there are all sorts of problems, like relying on the underlying data storage and data structure, not to mention that you are open to (very unlikely, to be fair) future updates to the underlying database structure, either directly via Magento upgrades or via (nasty) 3rd party modules. Not the mention the dangers of SQL injections or XSS attacks. (Though, you should always keep this in mind, with all 4 methods.)
As you are using the ObjectManager directly, I assume you won't mind these drawbacks, however, so I though I'd give you this option as well.
The basic pseudo-sql is:
select parent_id from <name of catalog_category_entity table> where entity_id in (<sanitized, comma-separated list of category ids);
First, acquire an instance of the \Magento\Framework\App\ResourceConnection class. You will use this to get the necessary table name for catalog_category_entity, as well as getting the database connection. Then you should sanitize your data and finally, the bind and execute the query and fetch your data.
/** #var \Magento\Framework\App\Connection */
$connection = $this->_resourceConnection->getConnection();
# Get prefixed table name of catalog_category_entity
$categoryEntityTableName = $this->_resourceConnection->getTableName('catalog_category_entity');
# Sanitize the $categoryIds array using a bit of overkill
array_walk_recursive($categoryIds, function(&$value, $key){
$value = filter_var($value, FILTER_SANITIZE_NUMBER_INT);
});
# Prepare a sql statement which fetches entity_id and parent_id
$preparedStatement = $this->connection->prepare('select entity_id, parent_id from ' . $categoryEntityTableName . ' where entity_id in (' . implode(',', array_fill(0, sizeof($categoryIds), '?')) . ')');
# Bind sanitized $categoryIds array to statement and execute said statement in one single step
$preparedStatement->execute($categoryIds);
# fetch result as a key-value pair array of entity_id=>parent_id
$parentIds = $preparedStatement->fetchAll(\PDO::FETCH_KEY_PAIR);
# Iterate over results and print them out!
foreach ($parentIds as $categoryId => $parentId) {
echo 'Parent Category ID: ', (int)$parentId, PHP_EOL;
}
Footnote
I assume you are well aware of the pros and cons of using the ObjectManager directly, so I'll spare you the lecture ;-). However, for future reference I'll also have to state to future readers stumbling upon this answer that if they are unaware on how to acquire instances of the CategoryFactory, CategoryRepository, CollectionFactory or ResourceConnection classes, I highly recommend them to do so via the intended Dependency Injection mechanism.
I am having difficulty sorting my data results alphabetically when matching them with the User that has placed the item in their "Locker".
I have two queries; the first one searches the database for all of the items that the user placed in their 'locker', and the second query pulls the details of the item and sorts them into a list by which brand the items are.
I feel like there is a better way to do this rather than forcing the page to run the query once for each item, but am not sure the proper way to write out the mySQL in the most efficient way that works.
I think the solution would be to pull all IDs as an array, then somehow search and sort all of their associated brands in the second query.
I currently have:
//$lockerid is pulled earlier in the code based on which locker number is associated with this user
// Pull all of the items and their ids that are in this users locker
$userlockerquery= mysql_query("SELECT DISTINCT item_id FROM lockers WHERE user_id = '$profile_userid' AND locker_id ='$lockerid' ");
while($lockeritems=mysql_fetch_array($userlockerquery)){
$indi_item=$lockeritems[item_id];
$lockeritemdetails = mysql_query("SELECT DISTINCT brand FROM inventory WHERE id = '$indi_item' ");
$brands=mysql_fetch_array($lockeritemdetails );
$brandname=$brands[brand];
echo '<div>'.$brandname.'</div>';
}
Although the results do show up with all of the brands, My problem seems to be that since the query is ran once for each items id, it cannot have the list results talk to each other, and thus cannot have them ordered by ASC alphabetically, since the query is ran once per each item.
Also because of this, the DISTINCT flag does not have any effect, since it is not matching against any other results.
As an example, my results would return in divs in order of ID instead of brand, and repeating:
Nike
Puma
Puma
Converse
Rather than
Converse
Nike
Puma
Adding the ORDER BY flag to the second query did not help, so I figured I would try to ask here for some ideas. Please let me know if any other details are needed!
Maybe try something like this class. See if it will work for your needs. It's hard to check it without trying the sql queries, but provided I've written it properly, it should work.
class MyLocker
{
// Protected means that you can't use this variable outside of the functions/class
// so you can not use $myLocker->_array; It will throw an error
protected $_array;
// Construct is basically used as an auto-function. It will execute automatically
// when you create a new instance of the class so as soon as you do this:
// $myLocker = new MyLocker($_locker); you initiate the __construct
// When you label as public, you allow it to be used outside of itself
public function __construct($_array)
{
// When you set this variable, it is now open to use in all
// other functions in this class.
$this->_array = $_array;
}
// This is the method that will do everything
public function LockerContents()
{
// Loop through query. Since the $_array was set in the __construct
// it is available in this function as $this->_array
while($lockeritems = mysql_fetch_array($this->_array)){
// $brand is something we want to use in other functions but not
// outside the class so it is set here for use in the Fetch() function
$this->brand = $lockeritems['item_id'];
// We ant to use our Fetch() function to return our brand
$_brand = $this->Fetch();
// If brand available, set it to an array
if(!empty($_brand))
$array[] = $_brand;
}
if(isset($array)) {
// Sort the array
asort($array);
// Finally, we use the Display() function for the final output
$this->Display($array);
}
else { ?>
<div>Locker is empty.</div><?php
}
}
// Establish this as an in-class variable
protected $brand;
// Establish this as a public function incase we want to use it by itself
// To do so you would write $myLocker->Fetch(); outside of the class.
// Since you need $brand for this function to work, you would need to turn
// $brand from "protected" to "public" and write $myLocker->brand = 'whatever';
// before you run the $myLocker->Fetch();
public function Fetch()
{
$query = mysql_query("SELECT DISTINCT brand FROM inventory WHERE id = '".$this->brand."'");
$brands = mysql_fetch_array($query);
// Return brand
return (isset($brands['brand']))? $brands['brand']:"";
}
protected function Display($array)
{
if(is_array($array)) {
foreach($array as $object) { ?>
<div><?php echo $object; ?></div><?php
}
}
}
}
// You should be using mysqli_ or PDO for your db connections/functions.
$_locker = mysql_query("SELECT DISTINCT item_id FROM lockers WHERE user_id = '$profile_userid' AND locker_id ='$lockerid' ");
// If there are more than 0 rows, create locker.
if(mysql_num_rows($_locker) > 0) {
// Create new instance of the locker app
$myLocker = new MyLocker($_locker);
// Display the results
$myLocker->LockerContents();
}
I hope the title was descriptive enough, i wasn't sure how to name it.
Let's say i have the following code:
Class Movie_model {
public method getMoviesByDate($date) {
// Connects to db
// Gets movie IDs from a specific date
// Loop through movie IDs
// On each ID, call getMovieById() and store the result in an array
// When all IDs has looped, return array with movies returned from getMovieById().
}
public function getMovieById($id) {
// Get movie by specified ID
// Also get movie genres from another method
// Oh, and it gets movie from another method as well.
}
}
I always want to get the same result when getting a movie (I always want the result from getMovieById().
I hope you get my point. I will have many other functions like getMoviesByDate(), i will also have getMoviesByGenre() for example, and i want that to return the same movie info as getMovieById() as well.
It it "ok" to do it this way? I know this puts more load on the server and increases load time, but is there any other, better way that i don't know of?
EDIT: I clarified the code in getMoviesByDate() a bit. Also, getMovieByDate() is just an example. As i said, i will be calling methods like getMoviesByGenre() also.
EDIT: I'm currently running 48 database queries on the frontpage of my project, and the frontpage is still far from finished, so that number would at least triple when i'm done. Almost all queries take around 0.0002, but as the database keeps growing that number will rise dramatically i'm guessing. I need to change something.
I don't think it's good to work like this in this particular case. The function getMoviesByDate would return an amount of "n" movies (or movie ids) from a single query. For each id in this query you would have a separate query to get the movie by the specified ID.
This would mean if the first function would return 200 movies, you would run the getMovieById() function (and the query inside it) 200 times. A better practice (IMO) would be to just get all the info you require in the getMoviesByDate() function and return it as a collection.
It doesn't seem very logical to have getMoviesByDate() and getMoviesById() methods on a Movie class.
An alternative would be to have some sort of MovieManager class that does all of the retrieving, and returns Movie objects.
class MovieManager {
public function getMoviesByDate($date) {
// get movies by date, build an array of Movie objects and return
}
public function getMoviesByGenre($genre) {
// get movies by genre, build an array of Movie objects and return
}
public function getMovieById($id) {
// get movie by id, return Movie object
}
}
Your Movie class would just have properties and methods specific to a single movie:
class Movie {
public id;
public name;
public releaseDate;
}
It's OK to have separate methods for getting by date, genre etc etc, but you must ensure that you are not calling for the same records multiple times - in that case you will want a single query that could join the various tables you need.
Edit - after you have clarified your question:
The idea of getting movie IDs by date, then running them all through getMovieById() is bad! The movie data should be pulled when getting by date, so you don't have to hit the database again.
You can modified your getMovieById function. You can pass date as a parameter, the function should return the movies by their id and filtered by date.
To keep track which records you've already loaded into RAM previously you can use a base class for your models which saves the id's of the records already loaded and a reference to object the model object in the RAM.
class ModelBase {
/* contains the id of the current record, null if new record */
protected $id;
// keep track of records already loaded
static $loaded_records = Array();
public function __construct(Array $attr_values) {
// assign $attr_values to this classes attributes
// save this instance in class variable to reuse this object
if($attr_values['id'] != null) {
self::$loaded_records[get_called_class()][$attr_values['id']] = $this;
}
}
public static function getConcurrentInstance(Array $attr_values) {
$called_class = get_called_class();
if(isset(self::$loaded_records[$called_class][$attr_values['id']])) {
// this record was already loaded into RAM
$record = self::$loaded_records[$called_class][$attr_values['id']];
// you may need to update certain fields of $record
// from the data in $attr_values, because the data in the RAM may
// be old data.
} else {
// create the model with the given values
$record = new $called_class($attr_values);
}
return $record;
}
// provides basic methods to update records in ram to database etc.
public function save() {
// create query to save this record to database ...
}
}
Your movie model could look something like this.
Class MovieModel extends ModelBase {
// additional attributes
protected $title;
protected $date;
// more attributes ...
public static function getMoviesByDate($date) {
// fetches records from database
// calls getConcurrentInstance() to return an instance of MovieModel() for every record
}
public static function getMovieById($id) {
// fetches record from database
// calls getConcurrentInstance() to return an instance of MovieModel()
}
}
Other things you could do do decrease the load on the DB:
Only connect once to the database per request. There are also possibilities to share a connection to a database between multiple requests.
Index thefields in your database which get searched often.
only fetch the records you need
Prevent to load the same record twice (if it didn't change)
What is the best way of working with calculated fields of Propel objects?
Say I have an object "Customer" that has a corresponding table "customers" and each column corresponds to an attribute of my object. What I would like to do is: add a calculated attribute "Number of completed orders" to my object when using it on View A but not on Views B and C.
The calculated attribute is a COUNT() of "Order" objects linked to my "Customer" object via ID.
What I can do now is to first select all Customer objects, then iteratively count Orders for all of them, but I'd think doing it in a single query would improve performance. But I cannot properly "hydrate" my Propel object since it does not contain the definition of the calculated field(s).
How would you approach it?
There are several choices. First, is to create a view in your DB that will do the counts for you, similar to my answer here. I do this for a current Symfony project I work on where the read-only attributes for a given table are actually much, much wider than the table itself. This is my recommendation since grouping columns (max(), count(), etc) are read-only anyway.
The other options are to actually build this functionality into your model. You absolutely CAN do this hydration yourself, but it's a bit complicated. Here's the rough steps
Add the columns to your Table class as protected data members.
Write the appropriate getters and setters for these columns
Override the hydrate method and within, populate your new columns with the data from other queries. Make sure to call parent::hydrate() as the first line
However, this isn't much better than what you're talking about already. You'll still need N + 1 queries to retrieve a single record set. However, you can get creative in step #3 so that N is the number of calculated columns, not the number of rows returned.
Another option is to create a custom selection method on your TablePeer class.
Do steps 1 and 2 from above.
Write custom SQL that you will query manually via the Propel::getConnection() process.
Create the dataset manually by iterating over the result set, and handle custom hydration at this point as to not break hydration when use by the doSelect processes.
Here's an example of this approach
<?php
class TablePeer extends BaseTablePeer
{
public static function selectWithCalculatedColumns()
{
// Do our custom selection, still using propel's column data constants
$sql = "
SELECT " . implode( ', ', self::getFieldNames( BasePeer::TYPE_COLNAME ) ) . "
, count(" . JoinedTablePeer::ID . ") AS calc_col
FROM " . self::TABLE_NAME . "
LEFT JOIN " . JoinedTablePeer::TABLE_NAME . "
ON " . JoinedTablePeer::ID . " = " . self::FKEY_COLUMN
;
// Get the result set
$conn = Propel::getConnection();
$stmt = $conn->prepareStatement( $sql );
$rs = $stmt->executeQuery( array(), ResultSet::FETCHMODE_NUM );
// Create an empty rowset
$rowset = array();
// Iterate over the result set
while ( $rs->next() )
{
// Create each row individually
$row = new Table();
$startcol = $row->hydrate( $rs );
// Use our custom setter to populate the new column
$row->setCalcCol( $row->get( $startcol ) );
$rowset[] = $row;
}
return $rowset;
}
}
There may be other solutions to your problem, but they are beyond my knowledge. Best of luck!
I am doing this in a project now by overriding hydrate() and Peer::addSelectColumns() for accessing postgis fields:
// in peer
public static function locationAsEWKTColumnIndex()
{
return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS;
}
public static function polygonAsEWKTColumnIndex()
{
return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS + 1;
}
public static function addSelectColumns(Criteria $criteria)
{
parent::addSelectColumns($criteria);
$criteria->addAsColumn("locationAsEWKT", "AsEWKT(" . GeographyPeer::LOCATION . ")");
$criteria->addAsColumn("polygonAsEWKT", "AsEWKT(" . GeographyPeer::POLYGON . ")");
}
// in object
public function hydrate($row, $startcol = 0, $rehydrate = false)
{
$r = parent::hydrate($row, $startcol, $rehydrate);
if ($row[GeographyPeer::locationAsEWKTColumnIndex()]) // load GIS info from DB IFF the location field is populated. NOTE: These fields are either both NULL or both NOT NULL, so this IF is OK
{
$this->location_ = GeoPoint::PointFromEWKT($row[GeographyPeer::locationAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns().
$this->polygon_ = GeoMultiPolygon::MultiPolygonFromEWKT($row[GeographyPeer::polygonAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns().
}
return $r;
}
There's something goofy with AddAsColumn() but I can't remember at the moment, but this does work. You can read more about the AddAsColumn() issues.
Here's what I did to solve this without any additional queries:
Problem
Needed to add a custom COUNT field to a typical result set used with the Symfony Pager. However, as we know, Propel doesn't support this out the box. So the easy solution is to just do something like this in the template:
foreach ($pager->getResults() as $project):
echo $project->getName() . ' and ' . $project->getNumMembers()
endforeach;
Where getNumMembers() runs a separate COUNT query for each $project object. Of course, we know this is grossly inefficient because you can do the COUNT on the fly by adding it as a column to the original SELECT query, saving a query for each result displayed.
I had several different pages displaying this result set, all using different Criteria. So writing my own SQL query string with PDO directly would be way too much hassle as I'd have to get into the Criteria object and mess around trying to form a query string based on whatever was in it!
So, what I did in the end avoids all that, letting Propel's native code work with the Criteria and create the SQL as usual.
1 - First create the [get/set]NumMembers() equivalent accessor/mutator methods in the model object that gets returning by the doSelect(). Remember, the accessor doesn't do the COUNT query anymore, it just holds its value.
2 - Go into the peer class and override the parent doSelect() method and copy all code from it exactly as it is
3 - Remove this bit because getMixerPreSelectHook is a private method of the base peer (or copy it into your peer if you need it):
// symfony_behaviors behavior
foreach (sfMixer::getCallables(self::getMixerPreSelectHook(__FUNCTION__)) as $sf_hook)
{
call_user_func($sf_hook, 'BaseTsProjectPeer', $criteria, $con);
}
4 - Now add your custom COUNT field to the doSelect method in your peer class:
// copied into ProjectPeer - overrides BaseProjectPeer::doSelectJoinUser()
public static function doSelectJoinUser(Criteria $criteria, ...)
{
// copied from parent method, along with everything else
ProjectPeer::addSelectColumns($criteria);
$startcol = (ProjectPeer::NUM_COLUMNS - ProjectPeer::NUM_LAZY_LOAD_COLUMNS);
UserPeer::addSelectColumns($criteria);
// now add our custom COUNT column after all other columns have been added
// so as to not screw up Propel's position matching system when hydrating
// the Project and User objects.
$criteria->addSelectColumn('COUNT(' . ProjectMemberPeer::ID . ')');
// now add the GROUP BY clause to count members by project
$criteria->addGroupByColumn(self::ID);
// more parent code
...
// until we get to this bit inside the hydrating loop:
$obj1 = new $cls();
$obj1->hydrate($row);
// AND...hydrate our custom COUNT property (the last column)
$obj1->setNumMembers($row[count($row) - 1]);
// more code copied from parent
...
return $results;
}
That's it. Now you have the additional COUNT field added to your object without doing a separate query to get it as you spit out the results. The only drawback to this solution is that you've had to copy all the parent code because you need to add bits right in the middle of it. But in my situation, this seemed like a small compromise to save all those queries and not write my own SQL query string.
Add an attribute "orders_count" to a Customer, and then write something like this:
class Order {
...
public function save($conn = null) {
$customer = $this->getCustomer();
$customer->setOrdersCount($customer->getOrdersCount() + 1);
$custoner->save();
parent::save();
}
...
}
You can use not only the "save" method, but the idea stays the same. Unfortunately, Propel doesn't support any "magic" for such fields.
Propel actually builds an automatic function based on the name of the linked field. Let's say you have a schema like this:
customer:
id:
name:
...
order:
id:
customer_id: # links to customer table automagically
completed: { type: boolean, default false }
...
When you build your model, your Customer object will have a method getOrders() that will retrieve all orders associated with that customer. You can then simply use count($customer->getOrders()) to get the number of orders for that customer.
The downside is this will also fetch and hydrate those Order objects. On most RDBMS, the only performance difference between pulling the records or using COUNT() is the bandwidth used to return the results set. If that bandwidth would be significant for your application, you might want to create a method in the Customer object that builds the COUNT() query manually using Creole:
// in lib/model/Customer.php
class Customer extends BaseCustomer
{
public function CountOrders()
{
$connection = Propel::getConnection();
$query = "SELECT COUNT(*) AS count FROM %s WHERE customer_id='%s'";
$statement = $connection->prepareStatement(sprintf($query, CustomerPeer::TABLE_NAME, $this->getId());
$resultset = $statement->executeQuery();
$resultset->next();
return $resultset->getInt('count');
}
...
}