Call save() in model while using Propel Versionable - php

I just switched an existing Model to be Versionable.
After debugging quite a lot, I now realized that there are a quite a few cases that I use $this->save() in the model quite a few times and that this finally causing duplicate entries in the Version table.
Is the only way to prevent this by removing the -save() methods out of the model (I tried it out, it works) or is there another, more simple way to prevent the internal loop during version-creation and its saving?

Since you don't specify a version of Propel, I'm assuming the stable version 1.x, though the following might well apply to 2.x, which is in alpha5 at the time of writing.
As per this documentation, you can specify when it is appropriate to save a new version of model rows using this method:
class Book extends BaseBook
{
public function isVersioningNecessary($con = null)
{
return $this->getISBN() !== null && parent::isVersioningNecessary($con);
}
}
If that method returns false, the last version is overwritten; if it is true, a new version is created.
(The docs are slightly wrong in that I assume the parent should take a $con parameter: missing there, fixed here).

Related

Phalcon pdo excpetion model first

I have problem with phalcon framework namely with models methods...
As you know models has included methods find() and findFirst()
I have generated model with phalcon-dev tools and now I am trying to do Model::find on it but I am getting an exception but dont know why...
There is some more informations (e.g stacktrace) :
http://exception.mateuszmarzecki.pl/
You can try change methods in model file
public static function find($parameters = array())
{
return self::find($parameters);
}
Does not look like your passing it the right parms.
SELECT FROM `nacionality`
Notice that your not selecting any fields from the database, and that is why your getting the Exception.
So... after some time of debugging I've found the problem...
For the next generation... if you don't want to lose a week as I did. Just read carefully your application config.
Problems occurs because I missed table and column annotations as well.
In my application config I have something like:
$metaData->setStrategy(new \Engine\Db\Model\Annotations\Metadata());
so Phalcon was looking for annotations in my model files, more info about this you can find there:
https://forum.phalconphp.com/discussion/1933/column-types-for-model-annotations
Happy New Year

Is there a way to combine Archivable and Versionable behaviours in Propel 1?

I'm using Propel 1 in a fairly sizeable project, and the live version presently uses the Archivable behaviour. Thus, when a row is deleted, the behaviour transparently intercepts the call and moves the row into an archive table. This works fine.
I'm looking to change how this table works so that all saves are versioned. On a feature branch I have therefore removed the Archivable and added the Versionable behaviour. This drops the (table)_archive auto-generated table and adds a (table)_version table instead.
However, interestingly, the version table has a PK of (id, version) with a foreign key to the live table from id to id. This means that versions cannot exist without a live row, which is not what I want: I want to be able to delete a row and preserve the versions.
I thought this behaviour would act like Archivable i.e. the delete() method would be intercepted and modified from its usual approach. Unfortunately, as confirmed by the documentation, this method deletes the live row and any prior versions:
void delete(): Deletes the object version history
I tried mixing both Archivable and Versionable, but this seems to generate code that crashes in the Query API: it tries to call an archive() method that does not exist. I expect this behaviour mix was never intended to work (ideally it should be caught at schema build-time, and perhaps that will be fixed in Propel 2).
One solution is to try the SoftDelete behaviour instead of Archivable - this just marks records as deleted rather than moving them to another table. However this can be problematic because joining to a table with this behaviour can give the wrong counts for non-deleted rows (and the Propel team decided to deprecate it for this reason). It also feels like a rabbit-hole I don't want to go down, since the amount of refactoring may spiral out of control.
Thus, I am left with seeking a better approach to implement a versioning system that does not delete old versions when the live copy is deleted. I can do this manually by intercepting save and delete methods in the model class, but it seems a waste when Versionable nearly does what I want. Are there relevant parameters I can tweak, or is there value in writing a custom behaviour? A quick look at the template generation code for core behaviours makes me want to run away from the latter!
Here is the solution I came up with. My memory is rather hazy but it looks like I've taken the existing VersionableBehaviour and derived from it a new behaviour, which I have called HistoryVersionableBehaviour. It thus uses all the features of the core behaviour and then just overrides the generated delete with its own code.
Here is the behaviour itself:
<?php
// This is how the versionable behaviour works
require_once dirname(__FILE__) . '/HistoryVersionableBehaviorObjectBuilderModifier.php';
class HistoryVersionableBehavior extends VersionableBehavior
{
/**
* Reset the FKs from CASCADE ON DELETE to no action
*
* (I expect all future migration diffs will incorrectly try to re-add the constraint
* I manually removed from the migration that introduced versioning, may try to fix
* that another time. 'Tis fine for now).
*/
public function addVersionTable()
{
parent::addVersionTable();
$this->swapAllForeignKeysToNoDeleteAction();
$this->addVersionArchivedColumn();
}
protected function swapAllForeignKeysToNoDeleteAction()
{
$versionTable = $this->lookupVersionTable();
$fks = $versionTable->getForeignKeys();
foreach ($fks as $fk)
{
$fk->setOnDelete(null);
}
}
protected function addVersionArchivedColumn()
{
$versionTable = $this->lookupVersionTable();
$versionTable->addColumn(array(
'name' => 'archived_at',
'type' => 'timestamp',
));
}
protected function lookupVersionTable()
{
$table = $this->getTable();
$versionTableName = $this->getParameter('version_table') ?
$this->getParameter('version_table') :
($table->getName() . '_version');
$database = $table->getDatabase();
return $database->getTable($versionTableName);
}
/**
* Point to the custom object builder class
*
* #return HistoryVersionableBehaviorObjectBuilderModifier
*/
public function getObjectBuilderModifier()
{
if (is_null($this->objectBuilderModifier)) {
$this->objectBuilderModifier = new HistoryVersionableBehaviorObjectBuilderModifier($this);
}
return $this->objectBuilderModifier;
}
}
This needs something called a modifier, which is run at generation time to produce the base instance classes:
<?php
class HistoryVersionableBehaviorObjectBuilderModifier extends \VersionableBehaviorObjectBuilderModifier
{
/**
* Don't do any version deletion after the main deletion
*
* #param \PHP5ObjectBuilder $builder
*/
public function postDelete(\PHP5ObjectBuilder $builder)
{
$this->builder = $builder;
$script = "// Look up the latest version
\$latestVersion = {$this->getVersionQueryClassName()}::create()->
filterBy{$this->table->getPhpName()}(\$this)->
orderByVersion(\Criteria::DESC)->
findOne(\$con);
\$latestVersion->
setArchivedAt(time())->
save(\$con);
";
return $script;
}
}
The parent class has 798 lines, so my approach does seem to have saved a great deal of code, over building it all from scratch!
You'll need to specify the behaviour in your XML file for each table you want to activate it for:
<table name="job">
<!--- your columns... -->
<behavior name="timestampable" />
<behavior name="history_versionable" />
</table>
I am not sure whether my behaviour requires the presence of the timestampable behaviour - my guess is no, since it looks like the parent behaviour just adds columns to the versioned table and not the table itself. If you are able to try this without the timestampable behaviour do let me know how you get on, so I can update this post.
Finally you'll need to specify the location of your class so the Propel 1 custom autoloader knows where to find it. I use this in my build.properties:
# Declare a custom behaviour
propel.behavior.history_versionable.class = ${propel.php.dir}.WebScraper.Behaviours.HistoryVersionable.HistoryVersionableBehavior

Kohana 3.0.4 ORM $_created_column $_updated_column

I have a problem in my model declaration, in Kohana 3.0.4 with the fields $_created_column and $_updated_column.
The problem is that :
- When I create and update objects from my controllers, the fields in the database corresponding to $_created_column and $_updated_column declaration are modified, according to the current create/modification date, just as it should be.
When I create and update objects from the models (using DB::insert, DB::update) (this is the best practice -> handling data operations from models) the fields corresponding to the declaration are NOT updating.
The code for DB::update and DB::insert:
public function add_productimage($zoom, $particular, $thumbnail, $presentation, $product, $order){
$insert_id = DB::insert('product_image', array('zoom','particular','thumbnail','presentation','product','order'))
->values(array($zoom, $particular, $thumbnail,$presentation, $product, $order))
->execute();
return $insert_id;
}
Any idea why?
You are not using ORM for inserts and updates, thus these specific settings are not applied. You use DB Query Builder instead of ORM. Use ORM for inserts / updates and you will be then employing best practice in this case.
By the way: your version of Kohana (3.0.4) should be easily updated to 3.0.12 (the most up-to-date in 3.0.x line), and this will fix multiple bugs that existed in 3.0.4.
First of all I don't see any reason not to use ORM inside your method:
public function add_productimage($post)
{
$this->values($post);
$this->save();
}
This is the preffered way to go, since this way you'll have your model validated before saving.
To answer your question - have you tried doing it exactly the same way as docs say?
Oh and also make sure you point to the right table - in your example it's product_image while Kohana style is product_images. Maybe you forgot the add the 's' at the end.

CakePHP 2.0 Object not Array

I am currently a beginner in CakePHP, and have played around with CakePHP 1.3, but recently CakePHP 2.0 has been released.
So far I like it but the only thing is being a pain is the fact that it doesn't return Objects, rather it just returns arrays. I mean, it hardly makes sense to have to do $post['Post']['id']. It is (in my opinion) much more practical to just do $post->id.
Now after Google I stumbled upon this link, however, this kept generating errors about indexes not being defined when using the Form class (guessing this is because it was getting the objectified version rather than the array version).
I am following the Blog tutorial (already have followed it under 1.3 but going over it again for 2.0)
So, anyone know how to achieve this without it interfering with the Form class?
Hosh
Little known fact: Cake DOES return them as objects, or well properties of an object, anyway. The arrays are the syntactical sugar:
// In your View:
debug($this->viewVars);
Shwoing $this is a View object and the viewVars property corresponds with the $this->set('key', $variable) or $this->set(compact('data', 'for', 'view')) from the controller action.
The problem with squashing them into $Post->id for the sake of keystrokes is Cake is why. Cake is designed to be a heavy lifter, so its built-in ORM is ridiculously powerful, unavoidable, and intended for addressing infinity rows of infinity associated tables - auto callbacks, automatic data passing, query generation, etc. Base depth of multidimensional arrays depends on your find method, as soon as you're working with more than one $Post with multiple associated models (for example), you've introduced arrays into the mix and there's just no avoiding that.
Different find methods return arrays of different depths. From the default generated controller code, you can see that index uses $this->set('posts', $this->paginate()); - view uses $this->set('post', $this->Post->read(null, $id)); and edit doesn't use $this->set with a Post find at all - it assigns $this->data = $this->Post->read(null, $id);.
FWIW, Set::map probably throws those undefined index errors because (guessing) you happen to be trying to map an edit action, amirite? By default, edit actions only use $this->set to set associated model finds to the View. The result of $this->read is sent to $this->data instead. That's probably why Set::map is failing. Either way, you're still going to end up aiming at $Post[0]->id or $Post->id (depending on what you find method you used), which isn't much of an improvement.
Here's some generic examples of Set::map() property depth for these actions:
// In posts/index.ctp
$Post = Set::map($posts);
debug($Post);
debug($Post[0]->id);
// In posts/edit/1
debug($this-viewVars);
debug($this->data);
// In posts/view/1
debug($this-viewVars);
$Post = Set::map($post);
debug($Post->id);
http://api13.cakephp.org/class/controller#method-Controllerset
http://api13.cakephp.org/class/model#method-Modelread
http://api13.cakephp.org/class/model#method-ModelsaveAll
HTH.
You could create additional object vars. This way you wouldn't interfere with Cake's automagic but could access data using a format like $modelNameObj->id; format.
Firstly, create an AppController.php in /app/Controller if you don't already have one. Then create a beforeRender() function. This will look for data in Cake's standard naming conventions, and from it create additional object vars.
<?php
App::uses('Controller', 'Controller');
class AppController extends Controller {
public function beforeRender() {
parent::beforeRender();
// camelcase plural of current model
$plural = lcfirst(Inflector::pluralize($this->modelClass));
// create a new object
if (!empty($this->viewVars[$plural])) {
$objects = Set::map($this->viewVars[$plural]);
$this->set($plural . 'Obj', $objects);
}
// camelcase singular of current model
$singular = lcfirst(Inflector::singularize($this->modelClass));
// create new object
if (!empty($this->viewVars[$singular])) {
$object = Set::map($this->viewVars[$singular]);
$this->set($singular . 'Obj', $object);
}
}
}
Then in your views you can access the objects like so:
index.ctp
$productsObj;
view.ctp
$productObj->id;
All we're doing is adding 'Obj' to the variable names that Cake would already provide. Some example mappings:
Products -> $productsObj
ProductType -> $productTypesObj
I know this is not perfect but it would essentially achieve what you wanted and would be available across all of your models.
While I like the idea Moz proposes there are a number of existing solutions to this problem.
The quickest one I found is https://github.com/kanshin/CakeEntity - but it looks like you might need to refactor it for 2.x - there might even already be a 2.x branch or fork but I didn't look.
I also ran this question couple of time in my head. Now a few Cake based apps later, I see the benefit to be able to branch and merge (am, in_array etc.) result sets more conveniently with arrays than using objects.
The $Post->id form would be a sweet syntactic sugar, but not a real benefit over arrays.
You could write a function that iterates over your public propertys (see ReflectionClass::getProperties) and save it in an array (and return the array).
If you have access to the class, you can implement the ArrayAccess Interface and easily access your object as an array.
P.S.: Sorry, i've never used CakePHP but i think object-to-array conversion doesn't have to be a framework specific problem

Disable Cakephp's Auto Model "feature"

In cake 1.2 there is a feature that allows the developer to no have to create models, but rather have cake do the detective work at run time and create the model for you. This process happens each time and is neat but in my case very hazardous. I read about this somewhere and now I'm experiencing the bad side of this.
I've created a plugin with all the files and everything appeared to be just great. That is until i tried to use some of the model's associations and functions. Then cake claims that this model i've created doesn't exist. I've narrowed it down to cake using this auto model feature instead of throwing and error! So i have no idea what's wrong!
Does anybody know how to disable this auto model feature? It's a good thought, but I can't seem to find where i've gone wrong with my plugin and an error would be very helpful!
There's always the possibility to actually create the model file and set var $useTable = false.
If this is not what you're asking for and the model and its associations actually do exist, but Cake seems to be unable to find them, you'll have to triple check the names of all models and their class names in both the actual model definition and in the association definitions.
AFAIK you can't disable the auto modelling.
Cake 1.2
It's a hack and it's ugly cus you need to edit core cake files but this is how i do it:
\cake\libs\class_registry.php : line 127ish
if (App::import($type, $plugin . $class)) {
${$class} =& new $class($options);
} elseif ($type === 'Model') {
/* Print out whatever debug info we have then exit */
pr($objects);
die("unable to find class $type, $plugin$class");
/* We don't want to base this on the app model */
${$class} =& new AppModel($options);
}
Cake 2
Costa recommends changing $strict to true in the init function on line 95 of Cake\Utility\ClassRegistry.php
See Cake Api Docs for init
ClassRegistry.php - init function
Use
var $useTable = false;
in your model definition.
Delete all cached files (all files under app/tmp, keep the folders)
In most cases where models seem to be acting in unexpected ways, often they dont include changes you've made, it is because that cake is useing an old cached version of the model.
Uh...where do we start. First, as Alexander suggested, clear your app cache.
If you still get the same behaviour, there is probably something wrong with the class and/or file names.
Remember the rules, for controller:
* classname: BlastsController
* filename: blasts_controller.php
for model:
* classname: Blast
* filename: blast.php
Don't foget to handle the irregular inflections properly.

Categories