Tuesday, March 30, 2021

Converting an Application From Yii2 to Yii3

Converting Yii2 to Yii3

A Minimalist Conversion Example

I’ll admit, I put off playing with Yii 3 for quite a while because I was daunted by the quick peeks into the code base and sample applications. The differences between configuring and bootstrapping a Yii 2 application vs a Yii 3 application are significant. At first glance, the two versions of a base application are completely different. But, once you start digging in, the similarities become apparent and you can start to draw the connections between the Yii 2 methodologies and the Yii 3 counterparts. These are my first impressions and the results of my own investigations thus far. I expect things to continue to evolve as the Yii 3 project advances.

PLEASE NOTE: Yii 3 is *not* yet ready for production release. This blog is an attempt to get more people playing with it and correspondingly, more people involved with providing feedback and assistance in developing and supporting the project. If you are not feeling adventurous, table this project for when there is a stable release and official documentation. Check the Release Cycle for production timelines and updates.


When in doubt, read The Guide. There is a ton of great information in there. That said, the conversion instructions are kind of sparse thus far, but the points they raise are *huge* ones, and worth reading in depth, especially if you’re a novice with DI: https://github.com/yiisoft/docs/blob/master/guide/en/intro/upgrade-from-v2.md

This is not going to be an in-depth ‘how to convert every feature you currently have’ post -- this is a top level conversion of a very basic web application, which will hopefully be enough to get you moving on your own conversion journey of exploration and discovery (and contribution)!

If you’re installing a totally fresh project, the easiest way to get started, is to use the official composer create-project command, as documented on the yiisoft/app repo.

composer create-project --prefer-dist --stability=dev yiisoft/app 

If you’re stepping through a conversion, I suggest cloning or downloading the yiisoft/app project directly so that you can easily copy/paste the class files that you need as we reference them or as you come across them.

My first conversion attempt, I started by creating the new project application in a subdirectory of my current project, and while it was good, it also created a lot of things I didn’t understand and didn’t actually need, not to mention duplicating all my vendor folders and creating general chaos for my repository in the process. My second project conversion, I just downloaded the basic app and cherry picked in the files and components that were relevant so that I could move the pieces that were already built. Again, these are super simple websites I’m converting, so they aren’t going to need all the bells and whistles -- which is why Yii 3 appeals to me. Shrinking the vendor footprint and overhead is a great goal. Third time … I’ll let you know when I get there. :p

Less Inherent Magic - More Fine Control

One of the first immediate differences, is that you can no longer just reference a pre-built Application class in the index.php and have Yii magically do everything for you. (You can interact with the Yiisoft\Yii\Web\Application class directly instead, but your index.php will become quite messy). For now, you will need to manually create an ApplicationRunner class that will put all the pieces together for you. Full examples are available in the yiisoft/yii-demo and yiisoft/app repos, and I highly recommend digging into each of those to see full implementation examples for comparison. More on all of that later in the post …

Let’s begin by examining the most basic structural differences between the two project versions.

Structural Differences:

Yii v2 Yii v3
assets src/Asset
resources/asset
configconfig
componentssrc/Component
controllerssrc/Controller
messagesresources/message
migrations??
runtimeruntime
teststests
viewsresources/views
resources/layout
webpublic

Generally speaking, instead of having all the source code and related resources co-located, they’re now split at the root so that the class based files go under /src/ and the non-class based content goes under /resources/. This logical split between source code and resources helps to keep things tidy for static analysis and code coverage reports, among other things.

The /src/ folder needs to be configured in the composer.json as the PSR-4 source for App\\ namespace:

  "autoload": {
     "psr-4": {
        "App\\": "src"
     }
  },

The config folder is deceptive in that while it has not moved, it is completely redesigned to fully leverage the yiisoft/config package capabilities, allowing more fine tuned configuration management and less massive singular config files for big projects.

Using the Yiisoft/Config Package

Config files are now a combined responsibility of yourself, and the package maintainer. The yiisoft/config package blends the two with an intelligent process and defines how configuration defaults should be specified.

When the composer update is run, it will generate the config/packages/ folder and create a merge_plan.php file that records all the instructions on how your configurations should be tracked and merged. Again, this package is in active development and this information may change rapidly, so please be sure that you’re reading up at the package source.

If you require the package, without giving it any extra definitions in the composer.json file, it will default to using the config/packages/ folder, and will generate a merge_plan.php file that only incorporates the published package configurations. It will *not* however incorporate any of your local config options. This file is overwritten with every composer update, so do not manually adjust the merge_plan.php file. Adding a detailed config-plugin section will allow you to customize the generated merge_plan.php to include your own files and directories. Below is the sample extra definitions from the yiisoft/demo application, as of the time of this publishing:

	"extras": {
	...

        "config-plugin-options": {
            "output-directory": "config/packages"
        },
        "config-plugin": {
            "common": "config/common/*.php",
            "params": [
                "config/params.php",
                "?config/params-local.php"
            ],
            "web": [
                "$common",
                "config/web/*.php"
            ],
            "console": [
                "$common",
                "config/console/*.php"
            ],
            "events": "config/events.php",
            "events-web": [
                "$events",
                "config/events-web.php"
            ],
            "events-console": [
                "$events",
                "config/events-console.php"
            ],
            "providers": "config/providers.php",
            "providers-web": [
                "$providers",
                "config/providers-web.php"
            ],
            "providers-console": [
                "$providers",
                "config/providers-console.php"
            ],
            "routes": "config/routes.php"
        },
	...
}

If you need to modify the configuration for the config-plugin at any time, you can update your composer.json file and run composer update to get the updated config.

SIDEBAR: You can quickly and easily use the docker composer image to run your composer installations without installing composer locally. From your project root directory, where composer.json is located, run:

docker run --rm -it --volume $PWD:/app composer update

It will mount your local folder to the composer container, run the installations creating all the necessary files and folders, then remove itself once complete. You will be left with a nice clean vendor dir (and node_modules dir if applicable) as well as any other files that the installation created. Similarly, you can use the same image to require/remove/create-project etc., just pass it the proper command in place of ‘update’.

The ReadMe file in the yiisoft/config package explains what it’s doing in detail, but essentially you’re creating 8 new primary config files, which will be managed and expanded upon dynamically by the properly configured packages that you consume. The new config files will be used by the Config class when looking up the required configurations for the application to use.

The best way to get started with these, is to copy in the entire config/ folder (minus the packages directory) from the yiisoft/app as a starting point, then run the composer update command to synchronize all package distributions to what you need. There’s too much to go through each of the config files, just know that they’re setting up the DI for the Container to use, and poke through them as you have time and curiosity.

Note: it’s okay to leave your existing params array as it is, you don’t need to clear it; but, you will have to tweak it as you go so that the parameters are organized slightly differently in the long run. Ignore your old console.php and main.php files for now, as you can delete them later and use them as reference points during the conversion.

I believe if you’re not supporting any console commands, you can skip that portion and the config options for it above, but I have not tested that. It fits with the theory behind Yii 3 though, that you only include what you actually need for your project.

Once you’ve run composer update take a look through that merge_plan.php and you can start setting how it puts the pieces together.

Reconfiguring your source code

Building Your Own Magic

As stated earlier, the magic in the public index.php file requires a little more intervention on your part before you can install and launch your Yii application. Before you start, rename /web/ to /public/ -- or don’t. I see no reason you can’t keep it as web if it makes you happy and creates less heartburn for your apache or nginx admin. You just need to keep in mind that you’ve done so and be ready to catch anything unusual that happens as a result.

Bootstrapping the Application

The v2 index.php file should look something like the following:

defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

require(__DIR__ . '/../../vendor/autoload.php');
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
require(__DIR__ . '/../../common/config/aliases.php');

$config = yii\helpers\ArrayHelper::merge(
    ...
);

$application = new yii\web\Application($config);
$application->run();

Where yii\web\Application() does all the magic for you.

The v3 index.php is going to look something more like this:

use App\ApplicationRunner;

// PHP built-in server routing.
// ...sapi removed for brevity in the example here

require_once dirname(__DIR__) . '/vendor/autoload.php';

$runner = new ApplicationRunner();
// Development mode:
$runner->debug();
// Run application:
$runner->run();

So … where does the ApplicationRunner come from? It says App\ApplicationRunner - and App\\ is mapped in our composer.json autoloader to our /src/ folder -- and composer require doesn’t create that … While both are calling run(), one is requiring all our configs and the other doesn’t seem to need any of that?

Technically, you could just call the Yiisoft\Yii\Web\Application class directly, but using the ApplicationRunner will set up the configuration options and handle all the proper responses, wrapping it all up nice and tidy.

Thankfully, it’s all in the yiisoft/app repo, and you can copy it here: https://github.com/yiisoft/app/blob/master/src/ApplicationRunner.php

While you’re there, grab the Installer and ApplicationParameters classes as well, and https://github.com/yiisoft/app/blob/master/src/ApplicationParameters.php https://github.com/yiisoft/app/blob/master/src/Installer.php

These are the first of the ‘make it yourself’ files required to effectively leverage v3. Thankfully, we don’t have to actually make them ourselves, they’ve been provided in the app and demo sources (as have many other files). If you cloned/downloaded the repo for easy access, now is the time to copy/paste these three files in.

The names of the classes are pretty self explanatory, but just in case:

ApplicationRunner -
This class sets up all the error and event handling we expect from the Yiisoft\Yii\Web\Application class and establishes the DI container configurations based on the web configs that have been created.
Installer -
This class gets configured into the composer.json to automatically set the runtime and web/assets folder permissions after update. If you want to handle this manually, you can skip this class and the composer entry.
    "scripts": {
        ...
        "post-update-cmd": [
            "App\\Installer::postUpdate"
        ]
    },
ApplicationParameters -
This class is used to map the params you have specified via DI to be passed through to views and other areas of the application at need. If you want to handle this in another way, totally your prerogative. Update the values in the config/params.php file.
return [
    'app' => [
        'charset' => 'UTF-8',
        'locale' => 'en',
        'name' => 'Adventures in Programming',
    ],
…
];

And in the common/ folder there should be an application-parameters.php file which sets up the DI for the class.

Now, so far, while things are different, they’re just organizationally different. From here on out, the differences are more pronounced, as we’re looking at everything based off of Dependency Injection instead of using the Service Locator. If you haven’t already done so, this is another reminder to check out https://github.com/yiisoft/docs/blob/master/guide/en/intro/upgrade-from-v2.md for great links on understanding how they’re using DI with v3, and how to start doing so in your v2 applications even before conversion.

From here, we’ll just start at the top of the directory structure and work our way down.

Converting AssetBundles

Start by creating the /src/Asset folder to house your AssetBundle classes. Then you can move each customized AssetBundle to the new folder location, and update the namespace to App\Asset and update the parent class.

The new Yiisoft\Assets\AssetBundle class is structurally very similar to the Yii2 AssetBundle class, so to reuse your existing AssetBundle with the new parent class just update the properties that you have overwritten to use strict typing. If you’ve customized a lot, you may need to make more adjustments.

You will need to alter the basePath, baseUrl and sourcePath, etc. to reflect the update folder locations. The new configuration will look something like this for the basic AppAsset class

public ?string $basePath = '@assets';
public ?string $baseUrl = '@assetsUrl';
public ?string $sourcePath = '@resources/assets';

The files to be published should be moved to /resources/assets/*. You can still use a dist/ subdirectory with your asset resources, and/or multiple subfolders split by bundle, the above is just the basic example. You should be able to tweak it fairly quickly to meet your needs. Delete the old assets/ folder once you’ve cleared it out.

Converting Components

My simple apps had no custom components! But this should just be a matter of setting up /src/Component/ and moving your files to that new namespace and updating any dependencies accordingly. Use the Container and Config to set up DI properties for your components as necessary, or just use them the old instanced way until you have time to really dig into it. This post is long enough without diving in here, so expect a future post about creating App specific components at a later date.

Converting Controllers

To begin converting the controllers, we want to start by ensuring we’ve created the /src/Controller folder (namespace is App\Controller). Next, move the Controller files from the /controller/ folder to the new /src/Controller/ folder, and update the namespace to App\Controller for each. Next, remove the parent class (don’t panic!) and we have a few adjustments to make.

The controller will need to be set up for DI with a private ViewRenderer property, and a constructor that will establish that value. Please see the example SiteController in the yiisoft/app.

The constructor should look like the following:

    public function __construct(ViewRenderer $viewRenderer)
    {
        $this->viewRenderer = $viewRenderer->withControllerName('site');
    }

Add this and the private ViewRenderer $viewRenderer property to each of your controllers, updating the value passed to withControllerName() appropriately per controller.

The next step is to update your actions within the controllers to remove the action keyword from the method name, and specify a return value of ResponseInterface.

So, the updated index action should look something like the following (also from the yiisoft/app):

    public function index(): ResponseInterface
    {
        return $this->viewRenderer->render('index');
    }

What about class based actions in an actions array? I don’t know yet -- haven’t tackled that problem so far.

As you convert each controller action, it’s a good time to pull up the config/routes.php file and add the route definition for the action you’re updating. This config file takes the place of the urlManager rules section of the old config.

The line to replace the site/index action route will look like this:

return [
    Route::get('/')->action([SiteController::class, 'index'])->name('home'),
];

Additional lines get inserted to the array as you convert the classes, like so:

Route::get('/about/')->action([AboutController::class,’index'])->name('about/index'),

Doing this as you convert each action will help keep track of what is done and what isn’t, but whatever works best for you, do that.

Move the views and messages over en masse:

Move the /views/layouts folder to /resources/layouts
Move the remaining /views/ folder to /resources/views
Move the messages/ folder (if you have one) to /resources/message

Convert your views and layouts

The best place to start is to review the default application layout file:
https://github.com/yiisoft/app/blob/master/resources/layout/main.php

Layout Conversion

For the most part, it looks very similar, but the details are different. The AssetBundles get registered to the assetManager, not $this:

$assetManager->register([
    AppAsset::class,
    CdnFontAwesomeAsset::class,
]);

And then the js/css/etc/ content gets explicitly built based on what has been registered:

$this->setCssFiles($assetManager->getCssFiles());
$this->setJsFiles($assetManager->getJsFiles());
$this->setJsStrings($assetManager->getJsStrings());
$this->setJsVar($assetManager->getJsVar());

Depending on what widgets you are using in your layout, you may need to tweak them. In general, all of the Html class methods have been updated to chain, rather than having all the properties passed through a massive array.

View File Conversion

The sample site/index is pretty straight forward, but gives you the important pieces of how to set the title using the ApplicationParameters and how to set breadcrumbs params etc.
https://github.com/yiisoft/app/blob/master/resources/views/site/index.php

/** @var App\ApplicationParameters $applicationParameters */
$this->params['breadcrumbs'] = '/';
$this->setTitle($applicationParameters->getName());

By default, your views don’t get passed much:

https://github.com/yiisoft/app/blob/master/config/packages/yiisoft/view/params.php

But, if you check out the yiisoft/yii-demo instead, you can see how to configure it so that all of your views will get the urlGenerator, urlMatcher, and assetManager:
https://github.com/yiisoft/yii-demo/blob/master/config/packages/yiisoft/view/params.php

The urlGenerator takes the place of the old Url helper class. So, generating a link will look something like this:

  echo \Yiisoft\Html\Html::a(
    ‘See More»', 
    $urlGenerator->generate('about/index')
  )
  ->class('btn btn-light')
  ->encode(false);

There’s a whole lot more to dig into after this, but this should be enough to get the ball rolling on converting your basic Yii 2 application to Yii 3. There is so much information and so many examples in the app and yii-demo repositories, I highly recommend that as you’re going if you hit a sticking point, pull those up and search for what you’re doing. Odds are you’ll find something ;)

Good luck, and thank a core contributor today!!

No comments:

Post a Comment