1
Stop flattening many-to-many collection accessors.
2
-------------------------------------------------
6
$product = new Product(1);
7
$newProduct = clone $product;
8
foreach ($product->getOs_Collection() as $os) {
9
$newProduct->addOs($os);
13
The above code updates the join fields for the product2os table, removing the oses from the other product.
19
// Generator TODO, gen getters for the primary key fields, e.g. getProductId(). getKeyId() should just be an alias OR better, build from the self::$primaryKeyDefinition = array('product_id');
22
setKeyId UPDATED -- now handles multi-key while allowing old usage.
23
getKeyId UPDATED -- now handles multi-key while allowing old usage (return just value for single-key).
24
hasKeyId UPDATED -- now handles multi-key PKs.
25
getKeyName --- Removing usage of this function internally to Cough. Should remove it completely though, because all other code that might want to use it will have to check if it is an array or not, so they may as well just getPkFieldNames() and iterate through.
34
The primary key values will be stored in the $fields array.
36
The primary key field info will exist in the $fieldDefintions array, just like other fields.
38
Which fields comprise the primary key will be stored in $pkFieldNames
40
To get the primary key "id", getKeyId will still work and will return an array of [field_name] => [field_value] pairs when the PK is multi-key.
42
You can still call setKeyId, but it will act on all primary key fields, e.g. setting them all to null if you call setKeyId(null).
62
checkByArray -> checkByCriteria and changed second parameter to additionSql
71
* Fix CoughCollection save method so that CoughObject can just call save() on the collection object.
73
* Start making CoughCollection keyless...
75
* There are some issues with this.
76
* First, note that one of the reasons it needed to be keyless was because of complex joins (involving date ranges) -- there might be another way to solve that problem.
77
* Another reason was for new objects that have no PK yet. There has to be a system in place for removing those and for retrieving those.
78
* If we make it keyless, how will one remove items via a key? We have to search the list manually.
80
Regarding the above features, we may not be able to do them. At least, not without caveats.
82
First, there is an efficiency issue with going to KEYLESS. A remove based on an ID would have to iterate through all of them and compare the key ID. The benefit is that multi-key PK would be supported via that method. We might be able to solve that by having getKeyId flatting a multi-key PK either by imploding, building a hash, or serializing.
84
Second, not all entities have appropriate PKs. Consider the Wayne date range example. THIS IS NOW INVALID -- The wrong collection type was generated, and should either be fixed via smarter generator, generator configuration options (e.g. XML schema), OR simply overriding the defineCollections method to unset the incorrect type and add it back as the correct type, a one-to-many. Cough Generator WILL need to know to generate the join table CoughObject classes in that case, if it isn't modified to generate them all the time.
86
Third, can we accept that when you add a relationship where there is no PK on the collected element, we can't remove it? We could overcome this by allowing you to remove it via reference:
88
$order->addOrderLine($orderLineWithNoPkYet);
89
$order->removeOrderLine($orderLineWithNoPkYet); // will iterate through collection and remove it if found
95
if (!is_object($elementToAdd)) { // Array of fields or ID => construct object with that data.
96
$elementToAdd = new $this->elementClass($elementToAdd);
98
//$this->append($elementToAdd); // WRONG needs to go into a special array... but then foreach ($collection as $element) won't work :()
100
if ($elementToAdd->hasKeyId()) {
101
// add via key method
108
protected function remove($idOrObject) {
109
if (is_object($idOrObject)) {
110
if ($idOrObject->hasKeyId()) {
111
return $this->removeByKey($idOrObject->getKeyId());
113
return $this->removeByReference($idOrObject);
116
return $this->removeByKey($idOrObject);
120
protected function removeByKey($id) {
121
if (isset($this[$idOrObject])) {
122
$object = $this[$idOrObject];
123
unset($this[$idOrObject]);
125
$object = new $this->elementClass($idOrObject);
129
Ideas for removing: Don't have the Collection do anything except remove it from the "array", placing it into a removedElements array (which will be saved and keeps it out of the foreach loop in the mean time), and return a reference to the item removed. Then, the object can setup how removal should take place, e.g.
131
public function removeOrderLine($orderLine) {
132
$orderLine = $this->getCollection('orderLine')->remove($orderLine);
133
$orderLine->setOrderId(null);
137
This makes changing that logic a breeze (override the above), and decouples that logic from the collection, which really is a generic collection of order lines and not a Order_OrderLine_Collection.
139
Before we continue with that, demonstrate that many-to-many will work in the above way:
141
public function removeOs($os) {
142
$os = $this->getCollection('os')->remove($os);
143
$os->setJoinField('is_retired', 0); // old way, maybe still supported. If it is, it's simply going to do this:
144
// $os->getProduct2Os_Object()->setIsRetired(0);
148
Now, a complex many-to-many example:
155
person_id role_id school_id start_date end_date is_retired
156
Anthony student TTU 2005 2006 0
157
Anthony professor TTU 2006 2007 0
159
PK either has to be a separate column or a combination of everything. Let's assume it is a column on it's own.
161
AHA! The above should not be treated as a many-to-many but a one-to-many, e.g. $person->getEnrollment_Collection(). The generator currently treats this as a many to many, BUT the generator could be made to detect that we were going to generate TWO many-to-many using the same join table and thus prevent those from being generated and instead generate a one-to-many for the join table. Additionally, it could override the check function to join into the other tables, e.g.:
163
public function checkEnrollment_Collection() {
164
if ($this->hasKeyId()) {
168
, role.role_id AS `role.role_id`
169
, role.role_type AS `role.role_type`
170
, role.creation_datetime AS `role.creation_datetime`
171
, role.last_modified_datetime AS `role.last_modified_datetime`
172
, role.is_retired AS `role.is_retired`
173
, school.school_id AS `school.school_id`
174
, school.school_name AS `school.school_name`
175
, school.creation_datetime AS `school.creation_datetime`
176
, school.last_modified_datetime AS `school.last_modified_datetime`
177
, school.is_retired AS `school.is_retired`
180
INNER JOIN role USING (role_id)
181
INNER JOIN school USING (school_id)
183
' . $this->db->generateWhere($this->getPk(), null, '') . '
184
AND enrollment.is_retired = 0
189
$this->_checkCollection('enrollment', '', $sql);
192
2007-07-09 01:21:05: Looks good.
194
We may need to keep the tracking abilities of the collection in order for `removeAll()` to work (we were talking about having the removed elements returned back to parent and having items set, but when removeAll is called that doesn't happen...) There is a similar problem when adding. If we don't have all the keys yet, then we will have to make the sets later during the save process. This is sort of done now via the setForeignKeyFromParentPk() method, but does it handle things the way we want? Should all adds and removes be processed during the save process (the sets, that is)?
196
$this->setJoinField('is_retired', 0);
197
$this->save(); // go and save joinObject.
199
$this->setOrderId(null);
202
Just keep that in mind before moving the save logic to the collection.
208
* Can we deprecate getGetter and getSetter?
210
* Can we deprecate addToCollection, removeFromCollection, etc.? All that is require is to have generated calls to those function instead generate as either:
212
$this->getCollection($collectionName)->add($objectsOrIDs, $joinFields);
214
$this->getCollectionName_Collection()->add($objectsOrIDs, $joinFields);
216
* Remove: CoughCollection::getRandom()
218
* Address naming conflicts (getters are generated based on schema, but we also have getters in the class, e.g. getField, getInsertFields, getModifiedFields...)
220
* Consider putting things like '_Object' and '_Collection' as constants to a Cough class.
222
const OBJECT_SUFFIX = '_Object'; // empty string is more convenient, but only if your schema won't have naming collisions
223
const COLLECTION_SUFFIX = '_Collection'; // 's' is more convenient, but ditto.
226
* Update generator to generate getters for primary key fields and to comply with above changes.
228
* Solve issue with objects, i.e.
229
1. Is there a way to set an object? For example, we have a product object with null os_id and an os object pre-loaded in memory. How can we give that to the product object? Currently have to $product->setOsId($os->getOsId()); and then it will be re-pulled from the database when something tries to $product->getOs().
231
2. If we solve the above, go one step further and allow a single query to do this also, e.g.:
238
INNER JOIN os USING (os_id)
240
product.product_id = <product_id>
242
The initFields / setFields should pass on the os data... We may want to solve this by having some functions do the select conversions (os.* to `os.field1`, `os.field2`, etc.) and parse them out on DB retrieval into an associative array:
246
'field1' => 'value1',
247
'field2' => 'value2',
250
'field1' => 'value1',
251
'field2' => 'value2',
255
3. If (2) is solved, make sure it works for collections because that is probably where it would be used, i.e. pulling a bunch of orders and their accounts, contacts, shipping, and billing info with one query, and instantiating all the objects in memory with that data.
257
4. If (3) is solved, show an example of how to create a separate collection class that extends the default and adds the extra joins. Show how you can make other objects use that collection instead of the default, in case sometimes you just want to pull some data and sometimes you want all (if you always want all you can just override the default to always pull the data). Make sure that the _checkCollection call either works with the overridden collection definitions OR that you show how to override the checkBlah_Collection method.
259
5. This may already be done in order to solve the above, but take a look at the join field implementation as it could be solved automatically by this (pass the join_table.field1, etc.) to the join field OBJECT. The thing that may be left to do is (1) generator should gen the object and (2) specify how many-to-many collections work. Propel uses some getRef-ish syntax.
261
We might have solved this backwards?
263
$product->checkOs_Collection(); // same example, but now pretend it is many-to-many.
270
$product->addOs(new Os($rowDataWithBoth));
272
If 1-3 above are solved, then it will not set join fields, but instead construct a join field object and initialize it with the values. This is good, EXCEPT: it's a little confusing... It means we could leave everything alone, actually... BUT the product object should also have a reference to the product2os object, thus the confusion. SO, the way propel solves this is just by having the product and os objects be slave to the product2os object. I'm not sure I like this as code gets a little more confusing, e.g.:
275
I'm a product. I want my Oses.
277
The current Cough way:
279
$product->getOs_Collection(); // current way = not confusing, except MAYBE how to set join fields. ($os->setJoinField could still work, but we'd probably want to DEPRECATE that and require $os->getProduct2Os_Object()->setSomeField($someValue); Keep in mind though, if you are just trying to change join field stuff you could save some queries and just change it for crying out loud:
281
$join = new Product2Os();
282
$join->loadByCriteria(array('product_id' => 1, 'os_id' => 2));
283
if ($join->isInflated()) {
284
$join->setIsRetired(true);
288
We want to get rid of the above though...
290
$join = Produt2Os::retrieveByPk(array('product_id' => 1, 'os_id' => 2));
292
$join->setIsRetired(true);
294
// $join->delete(); // we should support this. A.S. can override the method to do nothing at AppCoughObject layer.
299
$osRefs = $product->getProduct2OsJoinOs();
301
foreach ($osRefs as $osRef) {
302
$oses[] = $osRef->getOs();
307
$product = new Product(1); // this used to check. That functionality is now deprecated.
309
Instead, construction does only one thing: initializes fields. There are two choices:
311
instantiate the object with an associative array of [field_name] => [field_value] pairs OR
313
instantiate the object with a single value as in the above example. The single value will be treated as the primary key id. For objects with a multi-field primary key, you should pass in an array.
315
This allows you to update an items values without running two queries (SELECT & UPDATE). It's not necessarily good practice, but is a side effect of making Cough's interface more consistent.
317
$product = new Product(1);
318
$product->setProductName('New Name');
328
* Move collections and objects into an array instead of attibutes so we have:
329
$collectionDefinitions
334
Revision 13 Commit message
335
--------------------------
337
Renamed create() to insert() (so we now have matching insert/update functions).
338
Cleaned up how modifiedFields work (used to set duplicate entries in the modifiedFields array).
339
Added some "@see" documentation (phpDoc).
340
Cleaned up checking functions for the object. Less code, more clear, and more functionality as a side-effect.
342
(can easily override getLoadSql to get custom SQL whereas before it required overriding defineCheckStatement AND overriding setKeyId to recall that method anytime the key was changed, etc...)
347
* Split out logic for which fields get written to the database on insert and update.
348
* This solves the A.S. issue without requiring them to override the entire insert function.
353
Cleaned up CoughObject::__construct() function.
355
Removed KEYLESS collection type -- KEYED full time.
357
Collection now does saving of objects instead of the logic living in CoughObject::save*ToManyCollection()
359
Simplified add/remove functions.
361
Removed getFirst, getLast, getRandom from CoughCollection (just use getPosition(); even that function is not that useful 80% of the time)
363
Removed CoughCollection::set() (and removeAll()) functions b/c the code that will handing the removing of collection elements will exist in the parent's remove function. It's also not that useful to begin with.
364
- WRONG, it is useful. Consider this example:
366
You have a form field with multiple check boxes, perhaps like this:
368
Select Platforms: [ ] Mac [ ] Win [ ] Linux
370
The markup for which might look like:
372
<input type="checkbox" name="platform[]" value="1" /> Mac
373
<input type="checkbox" name="platform[]" value="2" /> Win
374
<input type="checkbox" name="platform[]" value="3" /> Linux
376
Next, you want to easily save the state of checked items to the related entity (a Product). The setter can be used for this:
378
if (isset($_POST['platform'])) {
379
$platforms = $_POST['platform'];
381
$platforms = array(); // handles case when none where checked.
384
$product = new Product(1);
385
$product->setOs_Collection($_POST['platform']);
389
Now that we have proved the usefulness of the setCollection method, allow me to propose a solution. Continue to leave removeAll deprecated. Instead of having the set call removeAll and then add all the passed entities, instead have it run too loops:
391
// (I've written this somewhere else, BTW):
393
public function set($idsOrObjects) {
395
foreach ($this as $elementId => $element) {
396
if (isset($idsOrObjects[$elementId] || in_array($elementId, $idsOrObjects)) {
399
$this->remove($elementId);
404
foreach ($idsOrObjects as $key => $value) {
405
if ($value instanceof CoughObject) {
406
if (!$this->offsetExists($value->getKeyId())) {
410
if (!$this->offsetExists($value)) {
418
Added some magic method handling to CoughObject for objects, collections, and fields so that without generated functions (as long as the definitions are present) you can do:
421
setFieldName($value);
422
getObjectName_Object();
423
checkObjectName_Object();
424
getCollectionName_Collection();
425
checkCollectionName_Collection();
426
addObjectName($objectOrId, $joinFields); -- adds to the 'object_name' collection
427
removeObjectName($objectOrId); -- removes from the 'object_name' collection
430
WHATS LEFT should be:
432
* Enhance "collector" behavior:
433
CoughCollection::setCollectingObjectName('order');
434
CoughCollection::setCollector($this);
435
CoughObject::setCollectingObjectName($this->getCollectingObjectName());
436
CoughObject::setCollector($this);
437
$this->objects[$this->getCollectingObjectName()] = $collector;
439
* Notice no calls to setOrder. If we just didn't want to go through work of finding the method name, neverfear, for setObject('order', $collector) is here.
441
* Enhance join fields handling via generated objects.
443
* Clean up definitions arrays (how does Propel do it?) Or just update the generator and forget about it.
452
GenieCough - Great Power, little bitty living space.
453
GenieCough - Great Power, Small Footprint.