yiisoft / yii2

Yii 2: The Fast, Secure and Professional PHP Framework
http://www.yiiframework.com
BSD 3-Clause "New" or "Revised" License
14.23k stars 6.91k forks source link

fatal error in running migrations through web application in Yii 2.0.2 #6853

Closed hesna closed 9 years ago

hesna commented 9 years ago

I'm writing an step by step installer script which in one step it tries to run some database migrations located in a folder. To do this I create a console Yii application and use runAction() method to run migrations. here is the code:

    public static function initDatabase()
    {
        $migrationPath = self::getMigrationsDirectory();
        new \yii\console\Application(
            [
                'id' => 'Command runner',
                'basePath' => self::loadConfigFile('console', 'config.php')['basePath'],
                'components' => [
                    'db' => self::loadConfigFile('common', 'db.php'),
                ],
            ]
        );
        \Yii::$app->runAction('migrate', ['migrationPath' => $migrationPath, 'interactive' => false]);
    }

everything was fine until I upgraded the framework from 2.0.0 to 2.0.2, now these fatal errors occur:

Fatal error: Undefined constant 'STDOUT' in /Applications/MAMP/htdocs/app/vendor/yiisoft/yii2/console/Controller.php on line 61
Fatal error: Undefined constant 'STDOUT' in /Applications/MAMP/htdocs/app/vendor/yiisoft/yii2/console/ErrorHandler.php on line 70

I tested it with Yii 2.0.1 and it didn't work with that either. Yii 2.0.0 is fine though. I'm on MAMP environment with php 5.4.30, but tested it on Ubuntu and same error occurred. Am I missing something or there is an issue hiding somewhere?

yurii-github commented 9 years ago

defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); defined('STDOUT') or define('STDOUT', fopen('php://stdout', 'w'));

this is required by console app. not sure if it applies to your situation

hesna commented 9 years ago

Yeah thank you that solved the problem! But I don't get it, why it was working with version 2.0.0 and stopped working with version 2.0.1? and shouldn't this code be part of core framework? It's currently located in yii file which is Yii console bootstrap file, so gets executed whenever one uses terminal to run a console application. But there are scenarios that someone wants to create a console app within a web app, in that case those constants would be undefined. Or is there another way of doing this job that I'm not aware of?

cebe commented 9 years ago

creating a console app in web context can have various side effects, if we want to support this, there may be more things needed to consider.

I suggest that putting the code for creating the STDIN and STDOUT constants into init() of the console app class.

hesna commented 9 years ago

I suggest that putting the code for creating the STDIN and STDOUT constants into init() of the console app class.

If that's the case then how about I make the changes and create a PR?

cebe commented 9 years ago

feel free to send a PR but I'd like to review more places of the console support before I'll merge it.

yurii-github commented 9 years ago

by the way, for not CLI PHP output MUST BE 'php://output', otherwise it outputs only last line. at least it is for me in FastCGI.


i find another difficulty with migrations. i cannot combine them (in my case yii2 rbac and my schema). Here's similar approach to call migrations from web application. It works but like a card house so far

use yii\web\Controller;
use yii\console\controllers\MigrateController;
use yii\web\Application;
use yii\base\Exception;
class InstallController extends Controller
{.....

public function actionInstall()
{
    //app integration
    define('STDOUT', fopen('php://output', 'w'));

    $cfg_my = [
        'migrationTable' => 'yii2_migrations',
        'migrationPath' => \Yii::getAlias('@console-app/migrations'),
        'interactive' => 0,
        'templateFile' => \Yii::getAlias('@console-app/migrations/_template.php'),
        'db' => \Yii::$app->db
    ];
    $cfg_rbac = $cfg_my;
    $cfg_rbac['migrationPath'] = \Yii::getAlias('@yii/rbac/migrations');
    // cannot use \Yii::createObject($cfg). 'id' is not set!
    $my = new MigrateController(null, null, $cfg_my);
    $rbac = new MigrateController(null, null, $cfg_rbac);

    // suppresses echo
    $echo = function ($c) {
        ob_start();
        $c();
        return ob_get_clean();
    };

    $do = ['up','down'];
    $do = $do[1];

    try {
        ob_start();

        //1. init migration table. better way?
        $echo(function() use (&$rbac) { $rbac->actionHistory(1); });

        if ($do == 'up') {// UP ALL
            //2. check available migrations
            // --yii2 rbac
            echo $mgs = $echo(function() use (&$rbac) { $rbac->actionNew(); });
            $mgs = explode("\n", $mgs);
            if (preg_match('/^Found [\d] new migration[s]{0,1}:$/', $mgs[0])) { //NEW: apply by number
                $num = count($mgs) - 1;
                $rbac->actionUp($num);
            }
            // --my schema
            echo $mgs = $echo(function() use (&$my) { $my->actionNew(); });
            $mgs = explode("\n", $mgs);
            if (preg_match('/^Found [\d] new migration[s]{0,1}:$/', $mgs[0])) { //NEW: apply by number
                $num = count($mgs) - 1;
                $my->actionUp($num);
            }
        } else { // DOWN ALL
            //2. check installed migrations in reverse order
            // --my schema
            $mgs = $echo(function() use (&$my) { $my->actionHistory(); });
            $mgs = explode("\n", $mgs);
            if (preg_match('/^Showing the last [\d] applied migration[s]{0,1}:$/', $mgs[0])) { //NEW: apply by number
                $my->actionHistory();
                $num = count($mgs) - 1; //PROBLEM! cannot realise migration source!
                $num = 2; // i have 2 migrations
                $my->actionDown($num);
            }
            // --yii2 rbac
            $mgs = $echo(function() use (&$rbac) { $rbac->actionHistory(); });
            $mgs = explode("\n", $mgs);
            if (preg_match('/^Showing the last [\d] applied migration[s]{0,1}:$/', $mgs[0])) { //NEW: apply by number
                $num = count($mgs) - 1;
                $num = 1; // delete anything left
                $rbac->actionDown('all');
            }
        }

        echo '<br>-----------------------------<br>';
        echo  $rbac->actionHistory();

    } catch (Exception $e) {
        echo $e->getCode().':'.$e->getMessage();
    }
    catch (\Exception $e) {
        echo $e->getCode().':'.$e->getMessage();
    } finally {
        $ob = ob_get_clean();
        echo str_replace("\n", "<br/>", $ob);
    }
    die;
}
saada commented 9 years ago

This works for me! Would would guys like me to add this as a pull request to the docs? Or should this be part of the Yii2 core framework?

namespace common\components;

use Yii;
use yii\base\Component;
use yii\console\Application;

class General extends Component
{
    /**
     * Create and return an application instance
     * if the console app doesn't exist we create a new instance
     * otherwise, it returns the existing instance
     *
     * @return null|\yii\base\Module|Application
     */
    public static function switchToConsoleApp()
    {
        $config = self::getConsoleConfig();
        if (!$consoleApp = Yii::$app->getModule($config['id']))
            $consoleApp = new Application($config);

        // assuming that switching apps only happens when making console commands through the web app,
        // we have to redefine STDOUT to trick the console output into going straight to the web output
        define('STDOUT', fopen('php://output', 'w'));

        return $consoleApp;
    }

    public static function runConsoleActionFromWebApp($action, $params = [])
    {
        $webApp = Yii::$app;
        $consoleApp = self::switchToConsoleApp();
        $result = ($consoleApp->runAction($action, $params) == \yii\console\Controller::EXIT_CODE_NORMAL);
        Yii::$app = $webApp;
        return $result;
    }

    public static function getConsoleConfig()
    {
        return ArrayHelper::merge(
            require(Yii::getAlias('@common/config/main.php')),
            require(Yii::getAlias('@common/config/main-local.php')),
            require(Yii::getAlias('@console/config/main.php')),
            require(Yii::getAlias('@console/config/main-local.php'))
        );
    }
}

// example usage within any web controller
SiteController extends \yii\web\Controller
{
    public function actionRbacLoad() {
        General::runConsoleActionFromWebApp('rbac/load');
    }

    public function actionMigrate() {
        General::runConsoleActionFromWebApp('migrate', ['migrationPath' => '@console/migrations/', 'interactive' => false]);
    }
}