I'm working on a controller that will update a few tables. I am able to call my model from my controller and inside the model function I can make a begin and commit my query, it can rollback should an error happen.
Here is my sample:
Controller:
//update table when update button is clicked
if (!empty($this->data)) {
if ($this->Item->update($this->data)) {
$this->Item->create();
$this->redirect('/sample');
return;
} else {
$this->set('data', $this->data);
}
}
Model:
function update($data)
{
$this->begin($this);
if(!parent::save($data)) {
$this->rollback($this);
return false;
}
$this->commit();
return true;
}
Now this works fine. But what I need to do is to call another model in my controller like "$this->"ANOTHER MODEL HERE"->update()". I need to have rollback should a problem occur with either model transaction. What I'm thinking is to put a commit in my controller after both model call succeeds.
Much like this:
CONTROLLER PHP:
BEGIN TRANSACTION
->CALLS MODEL1
IF(MODEL1 == ERROR){
ROLLBACK
}
->CALLS MODEL2
IF(MODEL2 == ERROR){
ROLLBACK
}
COMMIT WHEN NO PROBLEM IS ENCOUNTERED
So is it possible to perform commit in controller? I am only able to do it in model. Thanks in advance!
So is it possible to perform commit in controller? I am only able to do it in model.
Yes, you can perform commit or rollback from within the controller. You need to get the datasource from one of your models first. In the controller code, simply reference one of the models you are using (assuming they are all in the same database):
$ds = $this->MyModelName->getdatasource();
Then you can begin, commit, and rollback to that datasource from within the controller.
$ds->begin();
// do stuff and save data to models
if($success)
{
$ds->commit();
}
else
{
$ds->rollback();
}
I actually have a rollback or commit in more than one place if I am bailing on the action and redirecting or finalizing in some step and redirecting. I just illustrate a simple case here.
Handling transactions in the controller makes the most sense to me since the controller action is where the transaction boundaries really reside conceptually. The idea of a transaction naturally spans updates to multiple models. I have been doing this using postgres as the back end database with Cake 2.2 and 2.3 and it works fine here. YMMV with other db engines though I suspect.
Trasactions are to be enhanced in futures versions of CakePHP, as you can see in this CakePHP Lighthouse ticket.
There are two possible solutions proposed there, and I am showing you a third one. You could create a custom method to save it, and manually commit the transactions:
public function saveAndUpdate($data) {
$ds = $this->getDataSource();
$ds->begin();
if ($this->save($data)) {
foreach(Array('Model1', 'Model2') as $model) {
if (!ClassRegistry::init($model)->update()) {
$db->rollback();
return false;
}
}
return $db->commit() !== false;
}
return false;
}
I wrote this code to illustrate how I though about your problem, although I didn't test.
More useful links:
Transactions at CakePHP Book
About CakePHP Behaviors
How to create Behaviors
I used commit within my if statements and rollback in my else statements. Since I was using two different models from within a controller, I created two different datasources
$transactiondatasource = $this->Transaction->getDataSource();
$creditcarddatasource = $this->Creditcard->getDataSource();
$transactiondatasource->begin();
$creditcarddatasource->begin();
if (CONDITION){
$creditcarddatasource->commit();
$transactiondatasource->commit();
CakeSession::delete('Cart');
} else {
$this->Session->setFlash(__('MESSAGE'));
$creditcarddatasource->rollback();
$transactiondatasource->rollback();
}
Related
I'm using Redis to cache different parts of my app. My goal is to not make a database query when the user is not logged in, as the app's content don't get updated regularly.
I cache the archive queries in my controller, however when I type hint a model in the controller, the model is retrieved from the database and then passed to the controller:
// My route
Route::get('page/{page:id}', [ PageController::class, 'show' ] );
// My controller
public function show ( Page $page ) {
// Here, the $page will be the actual page model.
// It's already been queried from the database.
}
What I'm trying to do is to try and resolve the page from the cache first, and then if the cache does not contain this item, query the database. If I drop the Page type-hint, I get the desired result ( only the id is passed to controller ) but then I will lose the benefit of IoC, automatic ModelNotFoundException, and more.
I've come across ideas such as binding the page model to a callback and then parsing the request(), but seems like a bad idea.
Is there any way to properly achieve this? I noticed that Laravel eloquent does not have a fetching event, which would be perfect for this purpose.
You can override the default model binding logic:
Models\Page.php
public function resolveRouteBinding($value, $field = null)
{
return \Cache::get(...) ?? $this->findOrFail($value);
}
Read more here https://laravel.com/docs/8.x/routing#customizing-the-resolution-logic
In order to check for existence of the data in Redis, you shouldn't type-hint the model into the controller's action. Do it like this:
public function show($pageId) {
if(/* check if cached */) {
// Read page from cache
} else {
Page::where('id', $pageId)->first();
}
}
Trying to learn events in Yii 2. I found a few resources. The link I got more attention is here.
How to use events in yii2?
In the first comment itself he explains with an example. Say for an instance we have 10 things to do after registration - events comes handy in that situation.
Calling that function is a big deal? The same thing is happening inside the model init method:
$this->on(self::EVENT_NEW_USER, [$this, 'sendMail']);
$this->on(self::EVENT_NEW_USER, [$this, 'notification']);
My question is what is the point of using events? How should I get full benefit of using them. Please note this question is purely a part of learning Yii 2. Please explain with an example. Thanks in advance.
I use triggering events for written (by default) events like before validation or before deletion. Here's an example why such things are good.
Imagine that you have some users. And some users (administrators, for example) can edit other users. But you want to make sure that specific rules are being followed (let's take this: Only main administrator can create new users and main administrator cannot be deleted). Then what you can do is use these written default events.
In User model (assuming User models holds all users) you can write init() and all additional methods you have defined in init():
public function init()
{
$this->on(self::EVENT_BEFORE_DELETE, [$this, 'deletionProcess']);
$this->on(self::EVENT_BEFORE_INSERT, [$this, 'insertionProcess']);
parent::init();
}
public function deletionProcess()
{
// Operations that are handled before deleting user, for example:
if ($this->id == 1) {
throw new HttpException('You cannot delete main administrator!');
}
}
public function insertionProcess()
{
// Operations that are handled before inserting new row, for example:
if (Yii::$app->user->identity->id != 1) {
throw new HttpException('Only the main administrator can create new users!');
}
}
Constants like self::EVENT_BEFORE_DELETE are already defined and, as the name suggests, this one is triggered before deleting a row.
Now in any controller we can write an example that triggers both events:
public function actionIndex()
{
$model = new User();
$model->scenario = User::SCENARIO_INSERT;
$model->name = "Paul";
$model->save(); // `EVENT_BEFORE_INSERT` will be triggered
$model2 = User::findOne(2);
$model2->delete(); // `EVENT_BEFORE_DELETE` will be trigerred
// Something else
}
I used yii blog from Yii Framework
I want clear data from like table after delete post
I used This code at Post Model
protected function afterDelete()
{
parent::afterDelete();
Like::model()->deleteAll('post_id='.$this->id);
}
But Not Delete data from Like Table after delete post
I suggest you put your code in a beforeDelete, not afterDelete, I think it is better to first delete related data and then the main object.
I also suggest opening CActiveRecord class from YiiFramework and look how exactly are the methods defined, see if you are using the right (public/protected) and if you need to return a "true" or no need to return anything.
Here's a working example of beforeDelete:
protected function beforeDelete() {
if (parent::beforeDelete()) {
// requests
Yii::app()->db->createCommand("DELETE FROM x2_oirequest_city WHERE city_id={$this->id}")->execute();
return true;
}
}
I have very similar code that is functioning without a hitch elsewhere in my Laravel app, but for some reason the below code is creating two $paypal_object database entries that are identical except for the payment_id field:
DonateController.php
public function mimic()
{
try {
//This block is the addOrder function from the pizza tutorial
$paypal_object = new Paypal();
//The user who is making the payment
$paypal_object->user()->associate(Auth::user());
$paypal_object->amount = 50.00;
$paypal_object->description = "new subscription";
$paypal_object->state = $payment->getState();
$paypal_object->payment_id = $payment->getId();
$paypal_object->save();
} catch (Exception $ex) {
$message = $ex->getMessage();
$messageType = "error";
}
exit;
}
Database Results (with test data)
I've condensed the above code from my controller a little. If you'd like to see more of my code, let me know and I'd be happy to provide it. My theory right now is that for some reason my mimic() method is getting run twice, but I'm not sure how to test to see if that's true beyond including this in the above code, but it's not giving me any results this time:
echo '<script>console.log(' . json_encode("Testing to see how many times this message appears.") . ');</script>';
Even if it is running twice, I'm not sure how that's happening or where to check. I'm guessing it could well be another problem entirely, but I don't know what.
Right now, I'm accessing this method by pinging its route:
Route::get('/paypal/mimic', 'DonateController#mimic');
but for every 1 ping I make, I get 2 database entries as shown in the above image.
Paypal model:
class Paypal extends Eloquent
{
/**
* Get the user that made the paypal payment.
*/
public function user()
{
# Defines an inverse one-to-many relationship
return $this->belongsTo('User');
}
}
User model:
public function paypal(){
# User has many paypal payments - although just one subscription
# Defines a one-to-many relationship
return $this->hasMany('Paypal');
}
Thanks in advance for any help.
We're using doctrine migrations and there often are problems when the migration contains multiple actions and one of them fails.
For example, if there is a migration adding 5 foreign keys and the 5th of them fails while fields aren't of the same length, fixing the error with the fields and regenerating migrations does not fix the whole thing, while now there is an error connected with the fact 4 of the keys already exists and don't allow the migration to run successfully.
Is there a stable way to use Doctrine migrations without such obvious problems as mentioned? We've used .sql files previosly, which aren't much better actually, but I'm pretty sure there is the right way of database versioning for a Doctrine-using project?
Generating migrations based on the difference between models and schema is great and I'd like to keep this possibility furthermore.
Thanks
I kind of solved this, the solution isn't all that nice, but still, I guess it will be useful to other people. I'm using CLI indeed I've already done the file making every migration update the number in the database, similar to the one in the Timo's answer before asking this question, but that still isn't very effective but worth doing anyway.
What I've done next kind of solves stuff, go to
doctrine/lib/Doctrine/Migration/Builder.php line 531. There is the definition of the default class every migration will extends. Since I'm using CLI and could not find a way to pass parameters to this place I've just replaced Doctrine_Migration_Base to another class MY_Doctrine_Migration_Base which is below.
If you're not using CLI I'd say you should try to pass options and not replace source.
So the below class extends Doctrine_Migration_Base and overwrites a bunch of methods to the ones, checking whether it's OK to make changes and then calling parent method to do them. It doesn't cover all the methods currently, just the ones I've encountered when I wrote this.
Now every migration Doctrine creates extends my class which is aimed at preventing the problems I mentioned originally.
<?php
class MY_Doctrine_Migration_Base extends Doctrine_Migration_Base {
public function __construct() {
$this->connection = Doctrine_Manager::getInstance()->getCurrentConnection();
}
public function addIndex($tableName, $indexName, array $definition) {
foreach ($this->connection->execute("SHOW INDEXES IN $tableName")->fetchAll(PDO::FETCH_ASSOC) as $index) {
if ($index['Key_name'] === $indexName.'_idx') {
echo "Index $indexName already exists in table $tableName. Skipping\n";
return;
}
}
parent::addIndex($tableName, $indexName, $definition);
}
public function removeColumn($tableName, $columnName) {
if ($this->column_exists($tableName, $columnName)) {
parent::removeColumn($tableName, $columnName);
} else {
echo "Column $columnName doesn't exist in $tableName. Can't drop\n";
}
}
public function createTable($tableName, array $fields = array(), array $options = array()) {
if ($this->connection->execute("SHOW TABLES LIKE '$tableName'")->fetchAll(PDO::FETCH_ASSOC)) {
echo "Table $tableName already exists. Can't create\n";
} else {
parent::createTable($tableName, $fields, $options);
}
}
public function addColumn($tableName, $columnName, $type, $length = null, array $options = array()) {
if (! $this->column_exists($tableName, $columnName)) {
parent::addColumn($tableName, $columnName, $type, $length, $options);
} else {
echo "Column $columnName already exists in $tableName. Can't add\n";
}
}
private function column_exists($tableName, $columnName) {
$exception = FALSE;
try { //parsing information_schema sucks because security will hurt too bad if we have access to it. This lame shit is still better
$this->connection->execute("SELECT $columnName FROM $tableName")->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $exception) {}
//if someone knows how to check for column existence without exceptions AND WITHOUT INFORMATION SCHEMA please rewrite this stuff
return $exception === FALSE;
}
}
Suggestions on how to improve this are welcome.
If you're using the doctrine-cli you can write your own migration task that backs up the database before the migration and restores the back up if the migration fails. I wrote something similar for our symfony/doctrine migrations.
If you put your task class in the correct directory the doctrine cli will display it in the list of available commands
Doctrine migrations can not handle this. Sorry to say that we all have these problems, because the migrations didn't run in a transaction.
You can improve this by adding a plugin. See: Blog-Post
The other possibility is to do a database backup before migrating and if something goes wrong you can reinstall the backup. You can automate this by a shell script