Migrating my CI4 blog to Laravel

This blog ran on CodeIgniter 4 for a few years. Recently I moved it to Laravel. This post is not a full tutorial, it's more about why I did it and the parts that gave me trouble.

Why I migrate

The blog was on CI4 since around 2022. It worked fine. But two things kept bugging me.

First, CI 4.3 is end of life now. No more updates, no more security fixes. Running an old framework on a public site is not a great idea.

Second, every time I want to add something — a login, file upload, a small API — I end up writing the same boring plumbing by hand. Sessions, validation, mail, all of it. I was kinda tired of it.

So I decided, ok, time to move to something with more stuff built in.

Why Laravel

I looked at a few options first.

I thought about just upgrading CI4 to the newer version. That fixes the "old framework" part, but not the "writing everything by hand" part. So, no.

I thought about going full JavaScript, like Next.js. I build stuff in Next before and I like it. But my blog is not a fancy app, it's mostly pages. Moving to Next means moving to a new host too, probably paying more. Too much for a personal blog.

Laravel made the most sense:

  • Same language, PHP. My hosting already runs it.
  • Same shape as CI4 — controllers, models, views. So the move feels more like translating than rewriting.
  • All the stuff I was tired of writing (auth, migrations, mail, rate limit) is already there.
  • Big community. When I get stuck, the answer is usually one search away.

Kind of a boring choice maybe. But boring is good when it's your own blog and you just want it to keep working.

The plan

I didn't want the site to go down. So the plan was simple.

  1. Build the new Laravel version in a separate folder, next to the old one.
  2. Point it at the same MySQL database the old blog already use, so all my posts stay.
  3. Rebuild the pages one by one — blog, admin, the small APIs.
  4. When it's ready, switch the domain's document root to the new folder.
  5. Keep the old CI4 app there, in case I need to go back.

The old blog stays online the whole time. The switch at the end is one setting in cPanel.

Most of the work was just porting code. The old models became Laravel models, the controllers stayed almost the same, the views I rewrote as Blade. Nothing exciting, just hours of moving things over.

Reusing the old database

The blog data is fine, I don't want to touch it. So the new app connects to the same DB and reads the existing tables as they are.

One surprise: my old CI4 migration files did not match what was actually in the database. Over the years I changed some columns by hand and never wrote a migration for it. The migration said one column name, but every query in the code used a different one.

So I stopped trusting the old migrations. I rebuilt the schema by reading the models and the queries, and wrote one Laravel migration that matches the real database. If you migrate an old project, check the real database, not the migration history.

Shared hosting fought me

This part cost me an evening. Shared hosting disables some PHP functions for security. On my plan, proc_open, exec and symlink are all off.

Composer needs proc_open for its post-install step, so a normal install crashes:

composer install --no-dev --optimize-autoloader
# Call to undefined function Composer\Util\escapeshellarg()

The packages actually installed fine, only the script after it failed. The fix is to skip Composer's scripts and run that step by hand:

composer install --no-dev --optimize-autoloader --no-scripts
php artisan package:discover

Then php artisan storage:link failed too, because it uses symlink(). So I just made the link myself:

ln -s ../storage/app/public public/storage

The migrations table clash

When I ran the Laravel migration against the shared database, it broke straight away:

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'migration'

Turns out CodeIgniter also has a table called migrations, but with totally different columns. Laravel looked at it and got confused.

Fix: rename the old table out of the way, let Laravel create its own, then tell Laravel my main migration is "already done" so it doesn't try to recreate tables that already exist.

RENAME TABLE migrations TO migrations_ci4_old;
php artisan migrate:install
php artisan migrate --force

After that, migrate only added Laravel's own tables (sessions, cache, jobs). The blog tables were left alone.

The folder that ate my route

After deploy, mayicu.id/admin gave a 404. But /admin/login and /admin/article worked fine. Weird.

The reason: my admin CSS and JS live in a real folder called public/admin/. Apache serves a real file or folder before it ever hands the request to Laravel. This line in .htaccess is why:

RewriteCond %{REQUEST_FILENAME} !-d

It means "if this is not a directory, send it to index.php". /admin is a directory, so it never reached my route. I renamed the folder to public/admin-assets/ and it was fixed.

Small stuff

The pagination looked broken after the move — big ugly arrows, numbers stacked on each other. Laravel's default pagination markup uses Tailwind classes, and my site has no Tailwind, so nothing was styled. I made a tiny custom pagination view with plain HTML and my own CSS.

I also removed some old features nobody used — a couple of calculators, an overlay thing, some dead APIs. A migration is a good time to clean house.

Deploying with git

Uploading files by hand for every change got old fast. Now the app is a git repo, and the server is a working copy connected to a private GitHub repo. Every deploy is just:

# my machine
git push

# server
sh deploy.sh

The script does git pull, install, and rebuilds the caches. Much better than dragging files into an FTP client (which i used to do).

Conclusion

The blog is on Laravel now. Same posts, same links, the old version still sitting there as a backup.

Was it worth it? For me, yes. Not because Laravel is magic, but because now adding a feature is fast instead of a chore, and I'm not running a framework that stopped getting patches.

If you have an old CI4 project and you're on PHP hosting, Laravel is a pretty easy move. Same ideas, just less work. The only real traps are the disabled functions on shared hosting and that migrations table name clash — everything else is just moving code.

Share this post on :