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.
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:
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:
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.
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:
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:
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
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:
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 RoR builtin server:
rails server
The result looks like:
Name and creator | Django was created by Adrian Holovaty and Simon Willison |
History | First version was released in 2005 |
Programming language | Python |
View technology | Django templates |
Deployment | Builtin server for test Real server like Gunicorn for production |
Other technical characteristics | |
Status | Django is actively maintained and by far the most widely used Python web framework |
Django is a complete server-side framework not just a web frontend framework.
For better comparison with other web frameworks the backend features 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:
django-admin startproject demo
cd demo
python manage.py startapp app
demo/app/templates/app/list.html (Django terminology: template, common MVC terminology: view):
<html>
<head>
<title>Django</title>
</head>
<body>
<h1>Django</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>{{ o.0 }}</td>
<td>{{ o.1 }}</td>
<td>{{ o.2 }}</td>
</tr>
{% endfor %}
</table>
<h2>Add data:</h2>
<form method="post" action="{% url 'add' %}">
{% csrf_token %}
F1: <input type="text" name="f1">
<br>
F2: <input type="text" name="f2">
<br>
F3: <select name="f3">
{% for opt in options %}
<option>{{ opt }}</option>
{% endfor %}
</select>
<br>
<input type="submit" value="Add"/>
</form>
</body>
</html>
Note that the output variables are o.0, o.1 and o.2 - not o[0], o[1] and o[3].
demo/app/views.py (Django terminology: views, common MVC terminology: controller with actions):
from django.http import HttpResponse
from django.shortcuts import render, redirect
from app.DB import get_all, add_one
def list(request):
data = get_all()
return HttpResponse(render(request, 'app/list.html', { 'data': data, 'options': ['', 'Verified', 'Not verified']}))
def add(request):
add_one(request.POST['f1'], request.POST['f2'], request.POST['f3'])
return redirect('list')
demo/app/DB.py (database):
import pymysql
def get_all():
con = pymysql.connect(host='localhost',user='root',password='hemmeligt',db='test')
c = con.cursor()
c.execute('SELECT f1,f2,f3 FROM t1demo')
data = c.fetchall()
c.close()
con.close()
return data
def add_one(f1, f2, f3):
con = pymysql.connect(host='localhost',user='root',password='hemmeligt',db='test')
c = con.cursor()
c.execute('INSERT INTO t1demo(f1,f2,f3) VALUES(%s,%s,%s)', (f1, f2, f3))
c.close()
con.commit()
con.close()
demo/demo/settings.py fragment:
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app',
]
...
demo/app/urls.py:
from django.urls import path
from . import views
urlpatterns = [
path("", views.list, name="list"),
path("add", views.add, name="add"),
]
Run using builtin server:
cd demo
python manage.py runserver 8000
The result looks like:
Version | Date | Description |
---|---|---|
1.0 | January 29th 2023 | Initial version |
1.1 | May 13th 2024 | Add Django section |
See list of all articles here
Please send comments to Arne Vajhøj