Migrating a Fegg 3 / 4 project
Fegg 5 separates the core from the project code and moves classes into namespaces. Projects built on v3 or v4 are migrated by hand. Once migrated, they are updated with fegg-cli core:update like any Fegg 5 project.
This page is a summary. The full steps, with the commands and the complete path tables, are in docs/migration-v3-v4-to-v5.md of the skeleton repository.
Work on a separate branch and test against a copy of the production database.
1. Find the changes the project made to the core
Find the skeleton version the project started from (version in package.json, or @version in fegg/Application.php). Check out the skeleton at that tag, copy the project's core files over it (fegg/, code/lib/, code/application/cms/, code/template/cms/, htdocs/cms/, htdocs/index.php, htdocs/.htaccess) and look at git diff. Keep the list; each item is moved to site/ in step 3.
2. Move files into the new structure
| v3 / v4 | Fegg 5 |
|---|---|
fegg/*.php, code/lib/ (DB_pdo, LH_FW, Validation, Tool, ...) | htdocs/_fegg/core/ (use the new version) |
Project values in fegg/settings.php | htdocs/_fegg/site/config/settings.php |
fegg/MyApplication.php | htdocs/_fegg/site/controller/Controller.php |
code/lib/Modifire.php | htdocs/_fegg/site/functions/modifier.php |
Other project libraries in code/lib/ | htdocs/_fegg/site/src/ (Site\) |
Shared CMS screens in code/application/cms/ (Index, Login, Logout, Media, Image, Multi, Migrate, Register) | htdocs/_fegg/core/controller/ (use the new version) |
Everything else in code/application/ | htdocs/_fegg/site/controller/ (cms/ becomes Cms/) |
Shared CMS templates in code/template/cms/ | htdocs/_fegg/core/template/cms/ (use the new version) |
Everything else in code/template/ | htdocs/_fegg/site/template/ |
code/config/, code/config/database/ (project tables) | htdocs/_fegg/site/config/, htdocs/_fegg/site/config/database/ |
code/data/ | htdocs/_fegg/site/data/ |
htdocs/index.php, fegg-cli | Recreate from core/dist/index.php.dist and core/dist/fegg-cli.dist |
htdocs/.htaccess | Add the Fegg rules from core/dist/htaccess.dist |
Then:
-
Copy
htdocs/_fegg/core/,htdocs/_fegg/.htaccessandhtdocs/cms/from the Fegg 5 skeleton, and runnpm run build:scss. -
Recreate the entry files. If the project changed the paths in
index.php, set the three constants (FEGG_HTML_DIR,FEGG_CORE_DIR,FEGG_SITE_DIR) to match (see Directory structure). -
Add the
FilesMatchblock and theRewriteRule (^|/)_fegg(/|$) - [F,L]line fromhtaccess.disttohtdocs/.htaccess. -
Move the project files with
git mv(keeps the history). -
Carry over the changed values of the old
fegg/settings.php(developer_ip,run_mode,maintenance_page_url, Basic authentication, languages) tosite/config/settings.php. Copysite/config/env.php.disttosite/config/env.phpon each development or staging server and set'FEGG_ENV' => 'development': without it the site runs as production and shows no error details (see Settings). -
Create
site/config/cms_menu.phpwith one block per post type that was in the old CMS menu. -
Replace the removed constants until
grep -rn "FEGG_DIR\|FEGG_CODE_DIR" htdocs/_fegg/sitefinds nothing:v3 / v4 Fegg 5 FEGG_CODE_DIR . '/config/...'FEGG_SITE_DIR . '/config/...'FEGG_CODE_DIR . '/data/...'FEGG_SITE_DIR . '/data/...'FEGG_CODE_DIR . '/application/...'FEGG_SITE_DIR . '/controller/...'FEGG_CODE_DIR . '/lib/...'A namespaced class in site/src/FEGG_DIR . '/...'Not needed (autoloaded)
At this point the site should work again. Old-style controllers (no namespace, extends MyApplication, getClass()) still run.
3. Move core changes into overrides in site/
Go through the list from step 1:
- Changed template of a shared CMS screen: put the changed file at the same path in
site/template/cms/. - Changed controller of a shared CMS screen: extend the core class (
class Media extends \Fegg\Controller\Cms\Media) and write only the changed methods (see Controller). - Code added to
MyApplication: move it to__init()ofsite/controller/Controller.php. - Changed shared library: create a subclass in
site/src/and use it.
When ./fegg-cli core:status prints No changes to core files., this step is done.
4. Move to namespaces
The Fegg 4 style keeps working through Fegg\Legacy, but every use writes Fegg deprecated: ... to the PHP error log, and the compatibility layer will be removed in Fegg 6. Use the log to fix the code step by step.
<?php
class News extends MyApplication
{
public function __init()
{
parent::__init();
$this->_lh = $this->getClass('LH_FW', 'news');
$this->_model = new Model(true, array('table' => 'cms_news', 'id' => 'news_id'));
}
}
<?php
namespace Site\Controller\Cms;
defined('FEGG_CORE_DIR') || exit;
use Fegg\Model;
class News extends Controller // Site\Controller\Cms\Controller: the CMS base
{
protected $_cmsSection = 'news'; // replaces new Helper('news') and the isAuth() check
public function __init()
{
parent::__init();
$this->_model = new Model(true, array('table' => 'cms_news', 'id' => 'news_id'));
}
public function update()
{
$this->requirePost(); // required: every action that changes data
// ...
}
public function delete($id)
{
$this->requirePost(); // deletes are POST forms with the CSRF token
// ...
}
}
CMS screens extend Site\Controller\Cms\Controller (copy site/controller/Cms/Controller.php from the skeleton), not Site\Controller\Controller: it checks the login and the CSRF token of every POST. Delete links in copied CMS list templates become POST forms (see Controller).
requirePost() in every action that changes dataFegg 4 had no such check, so copied CMS controllers do not have it. Add $this->requirePost() as the first line of every action that saves, deletes, sorts or otherwise writes (update, create, delete ...). The CSRF token is only checked on POST: an action without requirePost() still runs on a GET, so a link or an image on another site can change data while a CMS user is logged in. With it, a GET answers 405. Check every action with grep -n "public function" htdocs/_fegg/site/controller/Cms/*.php.
| v3 / v4 | Fegg 5 |
|---|---|
MyApplication | Site\Controller\Controller (public pages), Site\Controller\Cms\Controller (CMS screens) |
Application / CommandApplication | Fegg\Application / Fegg\CommandApplication |
Model, ModelWhere, ModelOrder, ... | Fegg\Model, Fegg\Model\Where, Fegg\Model\Order, ... |
DB_pdo | Fegg\Database\Pdo |
LH_FW | Fegg\Cms\Helper |
Validation | Fegg\Validation |
ReCAPTCHA, Turnstile | Fegg\Captcha\ReCAPTCHA, Fegg\Captcha\Turnstile |
Tool/Date, Tool/File, Tool/Mail, Tool/OEmbed, Tool/RSS, Tool/Trim, Tool/Upload | Fegg\Tool\Date, Fegg\Tool\File, ... |
$this->getClass('LH_FW', 'news') | new \Fegg\Cms\Helper('news') |
$this->getClass('Validation') | new \Fegg\Validation() |
$this->getClass('Tool/Upload') | new \Fegg\Tool\Upload() |
$this->getClass('MyLib/Foo') | new \Site\MyLib\Foo() in site/src/MyLib/Foo.php |
Inside a namespace, prefix PHP's own classes with \ (new \Exception, new \DateTime, \PDO::PARAM_INT).
You are done when grep -rn "getClass\|MyApplication\|extends Model\b" htdocs/_fegg/site finds nothing and the error log has no Fegg deprecated entries.
Behaviour changes to check
Fegg 5 fails loudly where Fegg 4 was silent. The full list is in the 5.0.0 section of htdocs/_fegg/core/UPGRADE.md. The ones most likely to affect an old project:
- Database errors throw
Fegg\Database\QueryException(the message includes the SQL and its parameters) instead of printing and callingexit. Uncaught, it is a logged HTTP 500. - Missing templates,
includetargets andtranscludeparents throwFegg\Template\TemplateException(HTTP 500) instead of printing "Can't Read" into the page../fegg-cli template:lintlists them all. - Template blocks are checked: an unclosed block or an extra
{{ end }}throws with the line number. - Sections are passed to the parent template as closures. Code that called the global
section_<name>()functions directly must be rewritten. updateWhere(),deleteWhere()anderaseWhere()with an empty condition throw instead of affecting every row. Pass'1 = 1'where every row was really meant.Model::order(),join()and thetable/id/visibleoptions accept only plain names. Move expressions toorderRaw().{{ options }}escapes labels and values, and{{ hidden }}escapes values.ORin conditions is put in brackets before the model adds the visible flag andvalid = 1. In Fegg 4,where('a = ?', 1)->where('b = ?', 2, 'OR')also returned unpublished and deleted rows matchingb; such lists can get shorter.- Arrays reach modifiers:
{{ $tags|feggImplode:', ' }}now prints the joined tags (it printed nothing). where()fragments withORare bracketed:where('category = ?', 1)->where('a = ? OR b = ?', ...)iscategory = 1 AND (a = ? OR b = ?). Fegg 4 matched rows of every category.- Actions: only a controller's own public methods can be called from a URL (see Controller). An action defined in
MyApplicationor a base controller is a 404: write it in each controller that uses it. An action named like a method ofFegg\Application(mail,in,redirect...) is a 404: rename it, or map the old URL with a route. - CMS saving actions with
requirePost()answer 405 to a GET (see above). Edit and delete forms in copied templates must usemethod="post". - Basic authentication with
basic_auth_scope = 1is checked before the controller's__init()and action, so redirects, downloads and direct output are protected too. - CMS sessions need the session's user to still exist in
cms_user: deleting a user ends their sessions. - No more
exit:displayPage(),redirect(),requirePost()and the CMS login check throwFegg\Http\Halt. Acatch (\Throwable $e)around them must rethrow it (see Controller). Replace your ownexitin controllers with$this->halt(). - Environment: error details need a
developer_ipaddress andFEGG_ENVother thanproduction(site/config/env.php).FEGG_TEST_ENVIRONMENTno longer looks at the host name. - Sessions and cookies: a session starts only when something is written to it.
setCookie()takes SameSite as its own argument, andunsetCookie()really deletes the cookie (see Parameters). - Security headers (
X-Frame-Options: SAMEORIGIN,nosniff,Referrer-Policy) are sent with every response. A page meant to be framed by another site calls$this->allowFraming()(see Security). - CMS login: logout is a POST. Copied CMS frame templates in
site/template/cms/add the classjs-cms-logoutto the logout link. Runfegg-cli migrationfor the login throttling tablecms_login_attempt./cms/login/create/...is gone: usefegg-cli user:create(see fegg-cli). - Uploads check the content against the extension; add the
@contentmessage to a copiedsite/config/file_error_message.php(see Upload). htdocs/.htaccess: remove the<IfModule mod_php5.c>block and the Apache 2.2Order/Deny from alllines (500 on Apache 2.4), and add the dotfile rules ofcore/dist/htaccess.dist.fegg-cli doctorwarns about the old lines.- Captcha keys and the database DSN move to
env.php: the skeleton'sdatabase_config.phpandcontact/contact_config.phpread them withfeggEnv(). Copied old files keep working; move the keys out of committed files when convenient. - Route-only controllers: controllers that implement
Fegg\Http\RouteOnly(such as the skeleton'sSite\Controller\Page) are only reached throughsite/config/route.php. Add a route such as$route['about/'] = 'page/render/about';for each static page that was reached as/page/render/...(see URL).
Additional work for v3 projects
- Mail (
Fegg\Tool\Mail) uses PHPMailer.sendWithFile(),setInputCharset()andsetSendCharset()were removed: add attachments withpushAttached()and callsend(). A failed send throwsPHPMailer\PHPMailer\Exception. - Trailing slash: URLs without an extension are redirected (301) to the same URL with a trailing
/. Check the keys inroute.phpand the links in templates. - PHP 8: fix deprecation warnings in the project code (for example passing
nullto string functions).
5. Check
-
./fegg-cli template:lintprints OK -
php -lreports no syntax errors for the PHP files underhtdocs/_fegg(excludingvendor/anddata/) -
./fegg-cli migrationagainst a copy of the production database only adds new core tables such ascms_login_attempt(no project table definition is missing) -
./fegg-cli core:statusprintsNo changes to core files. - Public pages, the contact form (through to sending), CMS login, create / edit / delete for each post type, and media and image uploads work
- Every post type appears in the CMS side menu and on the dashboard
- Every saving action in
site/controller/Cms/calls$this->requirePost(): opening its URL in the browser (a GET, e.g./cms/news/update/) answers 405 - No page links to an action named like a method of
Fegg\Application(mail,in,redirect...) or defined only in a base controller (both are 404 now) - The PHP error log has no warnings and no
Fegg deprecatedentries - After deploying,
./fegg-cli doctor https://<production URL>prints OK and the CMS dashboard shows no protection warnings
To roll back, redeploy the old branch. The table definitions are compatible, so the same database file works before and after the migration.