Web applications - MVC style (script languages)

Content:

  1. Introduction
  2. Plain PHP
  3. Laminas
  4. Laravel
    1. simple
    2. with form
  5. RoR
    1. simple
    2. with form

Introduction:

Additional disclaimer: I am a total beginner in PHP and I have literally no experience with Ruby at all, so I do not even know the languages well.

Plain PHP:

Strictly speaking this is not a web application framework as it is all custom code based on the basic building blocks of the technology.

But given that it is actually frequently used for MVC web applications, then it is relevant to cover.

Name and creator N/A
History PHP goes back to 1995
Programming language PHP
View technology PHP
Deployment Apache module
FastCGI
CGI
builtin server (development only)
Other technical characteristics
Status PHP is still actively maintained, but today it is probably mostly used for small projects - larger projects use a real framework

We have a PHP controller that forward or redirect to a PHP view.

list.php (view):

<html>
<head>
<title>Just plain old PHP</title>
</head>
<body>
<h1>Just plain old PHP</h1>
<h2>Show data:</h2>
<table border="1">
<tr>
<th>F1</th>
<th>F2</th>
<th>F3</th>
<?php
foreach($data as $o) {
?>
<tr>
<td><?= $o->f1 ?></td>
<td><?= $o->f2 ?></td>
<td><?= $o->f3 ?></td>
</tr>
<?php
}
?>
</tr>
</table>
<h2>Add data:</h2>
<form method="post" action="index.php">
F1: <input type="text" name="f1">
<br>
F2: <input type="text" name="f2">
<br>
F3: <select name="f3">
<?php
foreach($options as $opt) {
?>
<option><?= $opt ?></option>
<?php
}
?>
</select>
<br>
<input type="submit" value="Add"/>
</form>
</body>
</html>

index.php (controller):

<?php

require 'T1.php';
require 'DB.php';

$method = $_SERVER['REQUEST_METHOD'];
if($method == 'GET') {
    $data = DB::getAll();
    $options = array('', T1::VER, T1::NOTVER);
    require 'list.php';
}
if($method == 'POST') {
    $f1 = $_REQUEST['f1'];
    $f2 = $_REQUEST['f2'];
    $f3 = $_REQUEST['f3'];
    DB::saveOne(new T1($f1, $f2, $f3));
    header('Location: index.php');
}

?>

T1.php (data class):

<?php

class T1 {
    public const VER = 'Verified';
    public const NOTVER = 'Not verified';
    public $f1;
    public $f2;
    public $f3;
    public function __construct($f1 = 0, $f2 = '', $f3 = T1::NOTVER) {
        $this->f1 = $f1;
        $this->f2 = $f2;
        $this->f3 = $f3;
    }
}

?>

DB.php (database):

<?php

class DB {
    private static function getConnection() {
        $con = new PDO('mysql:host=localhost;dbname=Test', 'root', 'hemmeligt');
        $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $con->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        return $con;
    }
    public static function getAll() {
        $con = DB::getConnection();
        $stmt = $con->prepare('SELECT f1,f2,f3 FROM t1demo');
        $stmt->execute(array());
        $res = array();
        while($row = $stmt->fetch()) {
            $res[] = new T1($row['f1'], $row['f2'], $row['f3']);
        }
        return $res;
    }
    public static function saveOne($o) {
        $con = DB::getConnection();
        $stmt = $con->prepare('INSERT INTO t1demo(f1,f2,f3) VALUES(:f1,:f2,:f3)');
        $stmt->execute(array(':f1' => $o->f1, ':f2' => $o->f2, ':f3' => $o->f3));
    }
}

?>

Due to the PHP execution model it is actually easier to use an actual database than to simulate a database. The code use PDO and MySQL.

The application can be run using the builtin server as:

cd php
php -S 0.0.0.0:5000 -t .

The result looks like:

plain PHP

Laminas:

Name and creator ZF (Zend Framework) / Laminas was created by Zend
History First version was released in 2006 as ZF
In 2019 it became an open source project under Linux Foundation and renamed from ZF to Laminas
Programming language PHP
View technology processed PHP (.phtml files)
Deployment Apache module
FastCGI
CGI
builtin server (development only)
Other technical characteristics
Status Laminas is still actively maintained - it is not and never has been a top web MVC framework usage wise in the PHP world, but it has to some extent been considered the father of all other PHP web MVC frameworks and one to learn

Laminas is a complete server-side framework not just a web frontend framework. Among other features it has DB component based on TableDataGateway pattern for persistence.

For better comparison with other web frameworks the backend features including the DB component will not be used in this example.

Due to the geneation of files then a web application contains a complex but fixed directory structure and a lot of files that a beginner should not change.

Generation command:

php composer.phar create-project -s dev laminas/laminas-mvc-skeleton Demo

module/Application/view/layout/layout.phtml (master page):

<?= $this->doctype() ?> 
<html>
<head>
<?= $this->headTitle('Laminas / Zend Framework') ?>
</head>
<body>
<?= $this->content ?>
</body>
</html>

module/Application/view/application/index/list.phtml (view):

<h1>Laminas / Zend Framework</h1>
<h2>Show data:</h2>
<table border="1">
<tr>
<th>F1</th>
<th>F2</th>
<th>F3</th>
</tr>
<?php
foreach($data as $o) {
?>
<tr>
<td><?= $o->f1 ?></td>
<td><?= $o->f2 ?></td>
<td><?= $o->f3 ?></td>
</tr>
<?php
}
?>
</table>
<h2>Add data:</h2>
<form method="post" action="<?php echo $this->url('application', ['action' => 'add']); ?>">
F1: <input type="text" name="f1">
<br>
F2: <input type="text" name="f2">
<br>
F3: <select name="f3">
<?php
foreach($options as $opt) {
?>
<option><?= $opt ?></option>
<?php
}
?>
</select>
<br>
<input type="submit" value="Add"/>
</form>

module/Application/src/controller/IndexController.php (controller):

<?php

declare(strict_types=1);

namespace Application\Controller;

use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\ViewModel;

use Application\Model\DB;
use Application\Model\T1;

class IndexController extends AbstractActionController
{
    public function listAction()
    {
        return new ViewModel(['data' => DB::getAll(), 'options' => [ '', T1::VER, T1::NOTVER ] ]);
    }
    public function addAction()
    {
        $frmdat = $this->params()->fromPost();
        $f1 = $frmdat['f1'];
        $f2 = $frmdat['f2'];
        $f3 = $frmdat['f3'];
        DB::saveOne(new T1($f1, $f2, $f3));
        $this->redirect()->toRoute('application', [ 'action' => 'list' ]);
    }
}

?>

module/Application/src/model/T1.php (data class):

<?php

namespace Application\Model;

class T1 {
    public const VER = 'Verified';
    public const NOTVER = 'Not verified';
    public $f1;
    public $f2;
    public $f3;
    public function __construct($f1 = 0, $f2 = '', $f3 = T1::NOTVER) {
        $this->f1 = $f1;
        $this->f2 = $f2;
        $this->f3 = $f3;
    }
}

?>

module/Application/src/model/DB.php (primitive database):

<?php

namespace Application\Model;

use \PDO;

class DB {
    private static function getConnection() {
        $con = new PDO('mysql:host=localhost;dbname=Test', 'root', 'hemmeligt');
        $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $con->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        return $con;
    }
    public static function getAll() {
        $con = DB::getConnection();
        $stmt = $con->prepare('SELECT f1,f2,f3 FROM t1demo');
        $stmt->execute(array());
        $res = array();
        while($row = $stmt->fetch()) {
            $res[] = new T1($row['f1'], $row['f2'], $row['f3']);
        }
        return $res;
    }
    public static function saveOne($o) {
        $con = DB::getConnection();
        $stmt = $con->prepare('INSERT INTO t1demo(f1,f2,f3) VALUES(:f1,:f2,:f3)');
        $stmt->execute(array(':f1' => $o->f1, ':f2' => $o->f2, ':f3' => $o->f3));
    }
}

?>

module/Application/config/module.config.php fragment (route config):

    'router' => [
        'routes' => [
            'home' => [
                'type'    => Literal::class,
                'options' => [
                    'route'    => '/',
                    'defaults' => [
                        'controller' => Controller\IndexController::class,
                        'action'     => 'list',
                    ],
                ],
            ],
            'application' => [
                'type'    => Segment::class,
                'options' => [
                    'route'    => '/application[/:action]',
                    'defaults' => [
                        'controller' => Controller\IndexController::class,
                        'action'     => 'list',
                    ],
                ],
            ],
        ],
    ],

The master page is very primitive as the focus is on MVC not layout. Ideally there should not even be a master page, but too much change to remove it.

Due to the PHP execution model it is actually easier to use an actual database than to simulate a database. The code use PDO and MySQL.

Run using PHP builtin server:

php -S 0.0.0.0:5000 -t public/ public/index.php

The result looks like:

Laminas

Laravel:

Name and creator Laravel is an open source MVC framework created by Taylor Otwell
History First version was released in 2011
Programming language PHP
View technology processed PHP (.blade.html files)
Deployment Apache module
FastCGI
CGI
builtin server (development only)
Other technical characteristics
Status Laravel is actively maintained and the most widely used PHP web MVC framework today

Laravel is a complete server-side framework not just a web frontend framework. Among other features it has an Eloquent ORM for persistence.

For better comparison with other web frameworks the backend features including Eloquent will not be used in this example.

Eloquent is covered here.

Due to the geneation of files then a web application contains a complex but fixed directory structure and a lot of files that a beginner should not change.

simple:

Generation command:

php composer.phar create-project laravel/laravel:^8.0 Demo

resources/views/list.blade.php (view):

<html>
<head>
<title>Laravel Framework</title>
</head>
<body>
<h1>Laravel Framework</h1>
<h2>Show data:</h2>
<table border="1">
<tr>
<th>F1</th>
<th>F2</th>
<th>F3</th>
</tr>
@foreach($data as $o)
<tr>
<td>{{ $o->f1 }}</td>
<td>{{ $o->f2 }}</td>
<td>{{ $o->f3 }}</td>
</tr>
@endforeach
</table>
<h2>Add data:</h2>
<form method="post" action="{{ url('/add') }}">
<input type="hidden" name="_token" id="token" value="{{ csrf_token() }}"> <!-- needed to avoid HTTP 419 error on POST -->
F1: <input type="text" name="f1">
<br>
F2: <input type="text" name="f2">
<br>
F3: <select name="f3">
@foreach($options as $opt)
<option>{{ $opt }}</option>
@endforeach
</select>
<br>
<input type="submit" value="Add"/>
</form>
</body>
</html>

app/Http/Controllers/TestController.php (controller):

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

use App\Models\DB;
use App\Models\T1;

class TestController extends Controller
{
    public function list() {
        return view('list', ['data' => DB::getAll(), 'options' => [ '', T1::VER, T1::NOTVER ] ]);
    }
    public function add(Request $request) {
        $f1 = $request->input('f1');
        $f2 = $request->input('f2');
        $f3 = $request->input('f3');
        DB::saveOne(new T1($f1, $f2, $f3));
        return redirect('/');
    }
}

app/Models/T1.php (data class):

<?php

namespace App\Models;

class T1 {
    public const VER = 'Verified';
    public const NOTVER = 'Not verified';
    public $f1;
    public $f2;
    public $f3;
    public function __construct($f1 = 0, $f2 = '', $f3 = T1::NOTVER) {
        $this->f1 = $f1;
        $this->f2 = $f2;
        $this->f3 = $f3;
    }
}

?>

app/Models/DB.php (simple database):

<?php

namespace App\Models;

use \PDO;

class DB {
    private static function getConnection() {
        $con = new PDO('mysql:host=localhost;dbname=Test', 'root', 'hemmeligt');
        $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $con->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        return $con;
    }
    public static function getAll() {
        $con = DB::getConnection();
        $stmt = $con->prepare('SELECT f1,f2,f3 FROM t1demo');
        $stmt->execute(array());
        $res = array();
        while($row = $stmt->fetch()) {
            $res[] = new T1($row['f1'], $row['f2'], $row['f3']);
        }
        return $res;
    }
    public static function saveOne($o) {
        $con = DB::getConnection();
        $stmt = $con->prepare('INSERT INTO t1demo(f1,f2,f3) VALUES(:f1,:f2,:f3)');
        $stmt->execute(array(':f1' => $o->f1, ':f2' => $o->f2, ':f3' => $o->f3));
    }
}

?>

routes/web.php (route config):

<?php

use Illuminate\Support\Facades\Route;

use App\Http\Controllers\TestController;

Route::get('/', [TestController::class, 'list']);

Route::post('/add', [TestController::class, 'add']);

Due to the PHP execution model it is actually easier to use an actual database than to simulate a database. The code use PDO and MySQL.

Run using PHP builtin server:

php artisan serve

The result looks like:

Laravel

with form:

Generation command:

php composer.phar create-project laravel/laravel:^8.0 Demo
cd Demo
php composer.phar require laravelcollective/html

Edit config/app.php to enable laravelcollective/html.

resources/views/list.blade.php (view):

<html>
<head>
<title>Laravel Framework with form</title>
</head>
<body>
<h1>Laravel Framework with form</h1>
<h2>Show data:</h2>
<table border="1">
<tr>
<th>F1</th>
<th>F2</th>
<th>F3</th>
</tr>
@foreach($data as $o)
<tr>
<td>{{ $o->f1 }}</td>
<td>{{ $o->f2 }}</td>
<td>{{ $o->f3 }}</td>
</tr>
@endforeach
</table>
<h2>Add data:</h2>
{!! Form::open(array('method' => 'post', 'url' => '/add')) !!}
{!! Form::token() !!} <!-- needed to avoid HTTP 419 error on POST -->
F1: {!! Form::text('f1') !!}
<br>
F2: {!! Form::text('f2') !!}
<br>
F3: {!! Form::select('f3', $options) !!}
<br>
{!! Form::submit('Add') !!}
{!! Form::close() !!}
</body>
</html>

app/Http/Controllers/TestController.php (controller):

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

use App\Models\DB;
use App\Models\T1;

class TestController extends Controller
{
    public function list() {
        return view('list', ['data' => DB::getAll(), 'options' => [ '' => '', T1::VER => T1::VER, T1::NOTVER => T1::NOTVER ] ]);
    }
    public function add(Request $request) {
        $f1 = $request->input('f1');
        $f2 = $request->input('f2');
        $f3 = $request->input('f3');
        DB::saveOne(new T1($f1, $f2, $f3));
        return redirect('/');
    }
}

app/Models/T1.php (data class):

<?php

namespace App\Models;

class T1 {
    public const VER = 'Verified';
    public const NOTVER = 'Not verified';
    public $f1;
    public $f2;
    public $f3;
    public function __construct($f1 = 0, $f2 = '', $f3 = T1::NOTVER) {
        $this->f1 = $f1;
        $this->f2 = $f2;
        $this->f3 = $f3;
    }
}

?>

app/Models/DB.php (simple database):

<?php

namespace App\Models;

use \PDO;

class DB {
    private static function getConnection() {
        $con = new PDO('mysql:host=localhost;dbname=Test', 'root', 'hemmeligt);
        $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $con->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        return $con;
    }
    public static function getAll() {
        $con = DB::getConnection();
        $stmt = $con->prepare('SELECT f1,f2,f3 FROM t1demo');
        $stmt->execute(array());
        $res = array();
        while($row = $stmt->fetch()) {
            $res[] = new T1($row['f1'], $row['f2'], $row['f3']);
        }
        return $res;
    }
    public static function saveOne($o) {
        $con = DB::getConnection();
        $stmt = $con->prepare('INSERT INTO t1demo(f1,f2,f3) VALUES(:f1,:f2,:f3)');
        $stmt->execute(array(':f1' => $o->f1, ':f2' => $o->f2, ':f3' => $o->f3));
    }
}

?>

routes/web.php (route config):

<?php

use Illuminate\Support\Facades\Route;

use App\Http\Controllers\TestController;

Route::get('/', [TestController::class, 'list']);

Route::post('/add', [TestController::class, 'add']);

Due to the PHP execution model it is actually easier to use an actual database than to simulate a database. The code use PDO and MySQL.

Run using PHP builtin server:

php artisan serve

The result looks like:

Laravel

RoR:

Name and creator RoR (Ruby on Rails) / Rails was created by David Heinemeier Hansson
History First version was released in 2004
Programming language Ruby
View technology ERB
Deployment Builtin server
Other technical characteristics
Status RoR is actively maintained and by far the most widely used Ruby web MVC framework

Besides its own usage then RoR was the first web framework to introduce a long list of features that later became common:

Frameworks like Grails and Laravel are heavily inspired by RoR.

RoR is a complete server-side framework not just a web frontend framework. Among other features it has an "Active Record" model for persistence.

For better comparison with other web frameworks the backend features including Active Record will not be used in this example.

Due to the geneation of files then a web application contains a complex but fixed directory structure and a lot of files that a beginner should not change.

Generation command:

rails new Demo
cd Demo
rails generate controller Test list --skip-routes

simple:

app/views/layouts/application.html.erb (master page):

<!DOCTYPE html>
<%= yield %>

app/views/test/list.html.erb (view):

<html>
<head>
<title>RoR</title>
</head>
<body>
<h1>RoR</h1>
<h2>Show data:</h2>
<table border="1">
<tr>
<th>F1</th>
<th>F2</th>
<th>F3</th>
</tr>
<% for o in @data -%>
<tr>
<td><%=h o.f1 %></td>
<td><%=h o.f2 %></td>
<td><%=h o.f3 %></td>
</tr>
<% end %>
</table>
<h2>Add data:</h2>
<form method="post">
F1: <input type="text" name="f1">
<br>
F2: <input type="text" name="f2">
<br>
F3: <select name="f3">
<% for opt in @options -%>
<option><%=h opt %></option>
<% end %>
</select>
<br>
<input type="submit" value="Add"/>
</form>
</body>
</html>

app/controllers/test_controller.rb (controller):

class TestController < ApplicationController
    skip_before_action :verify_authenticity_token
    def list
        @data = DB::getAll
        @options = [ "", T1::VER, T1::NOTVER ]
    end
    def add
        f1 = params["f1"]
        f2 = params["f2"]
        f3 = params["f3"]
        o = T1.new(f1, f2, f3)
        DB::addOne(o)
        redirect_to "/"
    end
end

app/models/DB.rb (simulated database):

class T1
    VER = "Verified"
    NOTVER = "Not verified"
    def initialize(f1 = 0, f2 = "", f3 = "")
        @f1 = f1
        @f2 = f2
        @f3 = f3
    end
    def f1
        return @f1
    end
    def f2
        return @f2
    end
    def f3
        return @f3
    end
end

class DB
    @@db = [ T1.new(1, "A", T1::VER),
             T1.new(2, "BB", T1::VER),
             T1.new(3, "CCC", T1::VER),
             T1.new(4, "DDDD", T1::VER),
             T1.new(5, "EEEEE", T1::NOTVER) ]
    def self.getAll
        return @@db
    end
    def self.addOne(o)
        @@db = @@db.append(o)
    end
end

config/routes.rb (config routes):

Rails.application.routes.draw do
  get "/", to: "test#list"
  post "/", to: "test#add"
end

The master page is just a filler as the focus is on MVC not layout. Ideally there should not even be a master page, but too much change to remove it.

Run using PHP builtin server:

rails server

The result looks like:

RoR

with form:

app/views/layouts/application.html.erb (master page):

<!DOCTYPE html>
<%= yield %>

app/views/test/list.html.erb (view):

<html>
<head>
<title>RoR with real form</title>
</head>
<body>
<h1>RoR with real form</h1>
<h2>Show data:</h2>
<table border="1">
<tr>
<th>F1</th>
<th>F2</th>
<th>F3</th>
</tr>
<% for o in @data -%>
<tr>
<td><%=h o.f1 %></td>
<td><%=h o.f2 %></td>
<td><%=h o.f3 %></td>
</tr>
<% end %>
</table>
<h2>Add data:</h2>
<%= form_with  method: :post do |f| %>
F1: <%= f.text_field :f1 %>
<br>
F2: <%= f.text_field :f2 %>
<br>
F3: <%= f.select :f3, @options %>
<br>
<%= f.button "Add" %>
<% end %>
</body>
</html>

app/controllers/test_controller.rb (controller):

class TestController < ApplicationController
    def list
        @data = DB::getAll
        @options = [ "", T1::VER, T1::NOTVER ]
    end
    def add
        f1 = params[:f1]
        f2 = params[:f2]
        f3 = params[:f3]
        o = T1.new f1, f2, f3
        DB::addOne o
        redirect_to "/"
    end
end

app/models/DB.rb (simulated database):

class T1
    VER = "Verified"
    NOTVER = "Not verified"
    def initialize(f1 = 0, f2 = "", f3 = "")
        @f1 = f1
        @f2 = f2
        @f3 = f3
    end
    def f1
        return @f1
    end
    def f2
        return @f2
    end
    def f3
        return @f3
    end
end

class DB
    @@db = [ T1.new(1, "A", T1::VER),
             T1.new(2, "BB", T1::VER),
             T1.new(3, "CCC", T1::VER),
             T1.new(4, "DDDD", T1::VER),
             T1.new(5, "EEEEE", T1::NOTVER) ]
    def self.getAll
        return @@db
    end
    def self.addOne(o)
        @@db = @@db.append(o)
    end
end

config/routes.rb (config routes):

Rails.application.routes.draw do
  get "/", to: "test#list"
  post "/", to: "test#add"
end

The master page is just a filler as the focus is on MVC not layout. Ideally there should not even be a master page, but too much change to remove it.

Run using PHP builtin server:

rails server

The result looks like:

RoR

Article history:

Version Date Description
1.0 January 29th 2023 Initial version

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj