Skip to main content

PHP Laminas Basics

Introduction​

Laminas is a modern, open-source PHP framework for building web applications and services. It's the successor to the popular Zend Framework, maintaining the same high-quality codebase while operating under an open governance model through the Linux Foundation.

In this tutorial, we'll explore the fundamentals of Laminas, understand its architecture, and create a simple application to demonstrate its capabilities. By the end, you'll have a solid foundation to start building robust PHP applications using the Laminas framework.

What is Laminas?​

Laminas is a collection of PHP packages that together form a powerful framework for web application development. The project was created in 2019 when Zend Framework was transitioned to the Linux Foundation and rebranded. Laminas retains the enterprise-grade quality and features of Zend Framework while embracing a more community-driven approach.

Key features of Laminas include:

  • Modular architecture: Use only what you need
  • MVC implementation: Clean separation of concerns
  • Service management: Dependency injection container
  • Event-driven programming: Flexible application flow
  • Database abstraction: Simple database interactions
  • Authentication and authorization: Secure your applications
  • Form handling: Validation and filtering
  • REST API support: Build modern web services

Laminas Framework Structure​

Laminas is composed of three main sub-projects:

  1. Laminas Components - Standalone libraries that can be used independently
  2. Laminas MVC - A full-stack MVC framework for web applications
  3. Mezzio (formerly Zend Expressive) - Middleware runtime for PSR-15 middleware applications

Let's visualize this structure:

Setting Up Your First Laminas Project​

Let's start by creating a new Laminas MVC application. We'll use Composer, which is the standard PHP dependency manager.

Prerequisites​

  • PHP 7.4 or higher
  • Composer installed
  • Basic PHP knowledge

Creating a New Project​

Open your terminal and run the following command:

bash
composer create-project laminas/laminas-mvc-skeleton my-laminas-app

This command creates a new Laminas MVC application in the my-laminas-app directory. After the installation completes, navigate to the project folder:

bash
cd my-laminas-app

Project Structure​

Here's what your newly created project structure looks like:

my-laminas-app/
├── config/ # Application configuration
├── data/ # Application data (logs, cache)
├── module/ # Application modules
│ └── Application/ # Default module
├── public/ # Public web directory
│ ├── css/
│ ├── img/
│ ├── js/
│ └── index.php # Application entry point
├── vendor/ # Dependencies (managed by Composer)
├── composer.json # Composer configuration
└── README.md # Project documentation

Running the Application​

You can run the application using PHP's built-in web server:

bash
php -S 0.0.0.0:8080 -t public

Now open your browser and navigate to http://localhost:8080. You should see the default Laminas welcome page.

Understanding the MVC Pattern in Laminas​

Laminas implements the Model-View-Controller (MVC) pattern, which separates your application into three main components:

  1. Models: Handle data and business logic
  2. Views: Present data to users
  3. Controllers: Process requests and coordinate between models and views

The Request Lifecycle​

Let's see how a request flows through a Laminas application:

Creating a Simple Module​

In Laminas, applications are divided into modules. Let's create a simple "Blog" module to understand this concept better.

1. Create Module Structure​

First, create the module directory structure:

bash
mkdir -p module/Blog/config
mkdir -p module/Blog/src/Controller
mkdir -p module/Blog/src/Model
mkdir -p module/Blog/view/blog/index

2. Create Module Class​

Create a file module/Blog/src/Module.php:

php
<?php

namespace Blog;

use Laminas\ModuleManager\Feature\ConfigProviderInterface;

class Module implements ConfigProviderInterface
{
public function getConfig()
{
return include __DIR__ . '/../config/module.config.php';
}
}

3. Create Module Configuration​

Create a file module/Blog/config/module.config.php:

php
<?php

namespace Blog;

use Laminas\Router\Http\Segment;
use Laminas\ServiceManager\Factory\InvokableFactory;

return [
'controllers' => [
'factories' => [
Controller\IndexController::class => InvokableFactory::class,
],
],
'router' => [
'routes' => [
'blog' => [
'type' => Segment::class,
'options' => [
'route' => '/blog[/:action[/:id]]',
'defaults' => [
'controller' => Controller\IndexController::class,
'action' => 'index',
],
],
],
],
],
'view_manager' => [
'template_path_stack' => [
'blog' => __DIR__ . '/../view',
],
],
];

4. Create a Controller​

Create a file module/Blog/src/Controller/IndexController.php:

php
<?php

namespace Blog\Controller;

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

class IndexController extends AbstractActionController
{
public function indexAction()
{
$posts = [
['id' => 1, 'title' => 'Introduction to Laminas', 'content' => 'This is a post about Laminas basics.'],
['id' => 2, 'title' => 'Working with Modules', 'content' => 'This post explains Laminas modules.'],
['id' => 3, 'title' => 'Laminas Forms', 'content' => 'Learn about form handling in Laminas.'],
];

return new ViewModel(['posts' => $posts]);
}
}

5. Create a View Template​

Create a file module/Blog/view/blog/index/index.phtml:

php
<div class="jumbotron">
<h1>Welcome to the Blog Module</h1>
<p>This is a simple blog module created with Laminas</p>
</div>

<div class="row">
<?php foreach ($posts as $post): ?>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-body">
<h2 class="card-title"><?= $post['title'] ?></h2>
<p class="card-text"><?= $post['content'] ?></p>
<a href="#" class="btn btn-primary">Read More</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>

6. Register the Module​

Open config/modules.config.php and add your module to the array:

php
return [
// ... other modules
'Application',
'Blog', // Add this line
];

Now navigate to http://localhost:8080/blog in your browser, and you should see your blog module in action!

Working with Forms​

Laminas provides a powerful form handling system. Let's create a simple contact form:

1. Create a Form Class​

Create a directory for forms:

bash
mkdir -p module/Blog/src/Form

Create a file module/Blog/src/Form/ContactForm.php:

php
<?php

namespace Blog\Form;

use Laminas\Form\Form;
use Laminas\Form\Element;
use Laminas\InputFilter\InputFilterProviderInterface;
use Laminas\Validator;

class ContactForm extends Form implements InputFilterProviderInterface
{
public function __construct($name = null)
{
parent::__construct('contact-form');

// Name field
$this->add([
'name' => 'name',
'type' => Element\Text::class,
'options' => [
'label' => 'Your Name',
],
'attributes' => [
'id' => 'name',
'class' => 'form-control',
'placeholder' => 'Enter your name',
'required' => true,
],
]);

// Email field
$this->add([
'name' => 'email',
'type' => Element\Email::class,
'options' => [
'label' => 'Your Email',
],
'attributes' => [
'id' => 'email',
'class' => 'form-control',
'placeholder' => 'Enter your email',
'required' => true,
],
]);

// Message field
$this->add([
'name' => 'message',
'type' => Element\Textarea::class,
'options' => [
'label' => 'Message',
],
'attributes' => [
'id' => 'message',
'class' => 'form-control',
'placeholder' => 'Enter your message',
'required' => true,
'rows' => 5,
],
]);

// Submit button
$this->add([
'name' => 'submit',
'type' => Element\Submit::class,
'attributes' => [
'value' => 'Send Message',
'class' => 'btn btn-primary',
],
]);
}

public function getInputFilterSpecification()
{
return [
'name' => [
'required' => true,
'filters' => [
['name' => 'StripTags'],
['name' => 'StringTrim'],
],
'validators' => [
[
'name' => 'StringLength',
'options' => [
'min' => 2,
'max' => 100,
'messages' => [
Validator\StringLength::TOO_SHORT => 'Name must be at least %min% characters',
Validator\StringLength::TOO_LONG => 'Name must not exceed %max% characters',
],
],
],
],
],
'email' => [
'required' => true,
'filters' => [
['name' => 'StripTags'],
['name' => 'StringTrim'],
],
'validators' => [
[
'name' => 'EmailAddress',
'options' => [
'messages' => [
Validator\EmailAddress::INVALID_FORMAT => 'Please enter a valid email address',
],
],
],
],
],
'message' => [
'required' => true,
'filters' => [
['name' => 'StripTags'],
['name' => 'StringTrim'],
],
'validators' => [
[
'name' => 'StringLength',
'options' => [
'min' => 10,
'max' => 1000,
'messages' => [
Validator\StringLength::TOO_SHORT => 'Message must be at least %min% characters',
Validator\StringLength::TOO_LONG => 'Message must not exceed %max% characters',
],
],
],
],
],
];
}
}

2. Update the Controller​

Add a new action to handle the contact form in module/Blog/src/Controller/IndexController.php:

php
public function contactAction()
{
$form = new \Blog\Form\ContactForm();
$message = '';

$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());

if ($form->isValid()) {
// Get filtered and validated data
$data = $form->getData();

// In a real application, you would process the form data here
// (send email, save to database, etc.)

$message = 'Thank you for your message! We will get back to you soon.';

// Reset form after successful submission
$form = new \Blog\Form\ContactForm();
}
}

return new ViewModel([
'form' => $form,
'message' => $message,
]);
}

3. Create a View Template​

Create a file module/Blog/view/blog/index/contact.phtml:

php
<div class="container">
<h1>Contact Us</h1>

<?php if (!empty($message)): ?>
<div class="alert alert-success">
<?= $message ?>
</div>
<?php endif; ?>

<?php
$form = $this->form;
$form->prepare();
echo $this->form()->openTag($form);
?>

<div class="form-group">
<?= $this->formLabel($form->get('name')) ?>
<?= $this->formElement($form->get('name')) ?>
<?= $this->formElementErrors($form->get('name')) ?>
</div>

<div class="form-group">
<?= $this->formLabel($form->get('email')) ?>
<?= $this->formElement($form->get('email')) ?>
<?= $this->formElementErrors($form->get('email')) ?>
</div>

<div class="form-group">
<?= $this->formLabel($form->get('message')) ?>
<?= $this->formElement($form->get('message')) ?>
<?= $this->formElementErrors($form->get('message')) ?>
</div>

<div class="form-group">
<?= $this->formElement($form->get('submit')) ?>
</div>

<?= $this->form()->closeTag() ?>
</div>

Now navigate to http://localhost:8080/blog/contact to see and use your contact form.

Working with Databases​

Laminas provides the laminas-db component for database operations. Let's see how to work with databases:

1. Configure Database Connection​

Update your config/autoload/global.php file:

php
return [
'db' => [
'driver' => 'Pdo',
'dsn' => 'mysql:dbname=laminas_tutorial;host=localhost',
'driver_options' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
],
],
'service_manager' => [
'factories' => [
'Laminas\Db\Adapter\Adapter' => 'Laminas\Db\Adapter\AdapterServiceFactory',
],
],
];

Create config/autoload/local.php for your local database credentials (make sure this file is in your .gitignore):

php
return [
'db' => [
'username' => 'your_username',
'password' => 'your_password',
],
];

2. Create a Blog Post Model​

Create a file module/Blog/src/Model/Post.php:

php
<?php

namespace Blog\Model;

class Post
{
public $id;
public $title;
public $content;
public $created;

public function exchangeArray(array $data)
{
$this->id = !empty($data['id']) ? $data['id'] : null;
$this->title = !empty($data['title']) ? $data['title'] : null;
$this->content = !empty($data['content']) ? $data['content'] : null;
$this->created = !empty($data['created']) ? $data['created'] : null;
}
}

3. Create a Table Gateway​

Create a file module/Blog/src/Model/PostTable.php:

php
<?php

namespace Blog\Model;

use Laminas\Db\TableGateway\TableGatewayInterface;
use RuntimeException;

class PostTable
{
private $tableGateway;

public function __construct(TableGatewayInterface $tableGateway)
{
$this->tableGateway = $tableGateway;
}

public function fetchAll()
{
return $this->tableGateway->select();
}

public function getPost($id)
{
$id = (int) $id;
$rowset = $this->tableGateway->select(['id' => $id]);
$row = $rowset->current();

if (!$row) {
throw new RuntimeException(sprintf(
'Could not find row with identifier %d',
$id
));
}

return $row;
}

public function savePost(Post $post)
{
$data = [
'title' => $post->title,
'content' => $post->content,
];

$id = (int) $post->id;

if ($id === 0) {
$data['created'] = date('Y-m-d H:i:s');
$this->tableGateway->insert($data);
return;
}

if (!$this->getPost($id)) {
throw new RuntimeException(sprintf(
'Cannot update post with identifier %d; does not exist',
$id
));
}

$this->tableGateway->update($data, ['id' => $id]);
}

public function deletePost($id)
{
$this->tableGateway->delete(['id' => (int) $id]);
}
}

4. Update Module Configuration​

Update module/Blog/config/module.config.php to include service configuration:

php
'service_manager' => [
'factories' => [
Model\PostTable::class => function($container) {
$tableGateway = $container->get(Model\PostTableGateway::class);
return new Model\PostTable($tableGateway);
},
Model\PostTableGateway::class => function($container) {
$dbAdapter = $container->get('Laminas\Db\Adapter\Adapter');
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new Model\Post());
return new TableGateway('posts', $dbAdapter, null, $resultSetPrototype);
},
],
],

Don't forget to add the necessary use statements at the top of the file:

php
use Laminas\Db\ResultSet\ResultSet;
use Laminas\Db\TableGateway\TableGateway;

5. Update the Controller​

Now update your IndexController to use the database:

php
<?php

namespace Blog\Controller;

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

class IndexController extends AbstractActionController
{
private $postTable;

public function __construct(PostTable $postTable)
{
$this->postTable = $postTable;
}

public function indexAction()
{
return new ViewModel([
'posts' => $this->postTable->fetchAll(),
]);
}
}

Also update the controller factory in module.config.php:

php
'controllers' => [
'factories' => [
Controller\IndexController::class => function($container) {
return new Controller\IndexController(
$container->get(Model\PostTable::class)
);
},
],
],

Practical Example: Building a Todo List Application​

Let's apply what we've learned to create a simple todo list application. We'll create:

  1. A database table for tasks
  2. Models to interact with the database
  3. Controllers to handle CRUD operations
  4. Views to display and manipulate tasks

1. Create the Database Table​

First, create a tasks table in your database:

sql
CREATE TABLE tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
description TEXT,
status ENUM('pending', 'completed') DEFAULT 'pending',
priority ENUM('low', 'medium', 'high') DEFAULT 'medium',
created DATETIME DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO tasks (title, description, priority) VALUES
('Learn Laminas', 'Study the basics of Laminas framework', 'high'),
('Create a sample application', 'Apply knowledge by building a simple app', 'medium'),
('Practice routing', 'Master Laminas routing system', 'low');

2. Create a Todo Module​

Set up a new module structure:

bash
mkdir -p module/Todo/config
mkdir -p module/Todo/src/Controller
mkdir -p module/Todo/src/Model
mkdir -p module/Todo/src/Form
mkdir -p module/Todo/view/todo/task

3. Create the Module Class​

Create module/Todo/src/Module.php:

php
<?php

namespace Todo;

use Laminas\ModuleManager\Feature\ConfigProviderInterface;

class Module implements ConfigProviderInterface
{
public function getConfig()
{
return include __DIR__ . '/../config/module.config.php';
}
}

4. Create Module Configuration​

Create module/Todo/config/module.config.php:

php
<?php

namespace Todo;

use Laminas\Router\Http\Segment;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Laminas\Db\ResultSet\ResultSet;
use Laminas\Db\TableGateway\TableGateway;

return [
'controllers' => [
'factories' => [
Controller\TaskController::class => function($container) {
return new Controller\TaskController(
$container->get(Model\TaskTable::class)
);
},
],
],
'router' => [
'routes' => [
'todo' => [
'type' => Segment::class,
'options' => [
'route' => '/todo[/:action[/:id]]',
'constraints' => [
'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
'id' => '[0-9]+',
],
'defaults' => [
'controller' => Controller\TaskController::class,
'action' => 'index',
],
],
],
],
],
'view_manager' => [
'template_path_stack' => [
'todo' => __DIR__ . '/../view',
],
],
'service_manager' => [
'factories' => [
Model\TaskTable::class => function($container) {
$tableGateway = $container->get(Model\TaskTableGateway::class);
return new Model\TaskTable($tableGateway);
},
Model\TaskTableGateway::class => function($container) {
$dbAdapter = $container->get('Laminas\Db\Adapter\Adapter');
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new Model\Task());
return new TableGateway('tasks', $dbAdapter, null, $resultSetPrototype);
},
],
],
];

5. Create Task Model​

Create module/Todo/src/Model/Task.php:

php
<?php

namespace Todo\Model;

class Task
{
public $id;
public $title;
public $description;
public $status;
public $priority;
public $created;

public function exchangeArray(array $data)
{
$this->id = !empty($data['id']) ? $data['id'] : null;
$this->title = !empty($data['title']) ? $data['title'] : null;
$this->description = !empty($data['description']) ? $data['description'] : null;
$this->status = !empty($data['status']) ? $data['status'] : 'pending';
$this->priority = !empty($data['priority']) ? $data['priority'] : 'medium';
$this->created = !empty($data['created']) ? $data['created'] : null;
}

public function getArrayCopy()
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'status' => $this->status,
'priority' => $this->priority,
'created' => $this->created,
];
}
}

6. Create Task Table Model​

Create module/Todo/src/Model/TaskTable.php:

php
<?php

namespace Todo\Model;

use Laminas\Db\TableGateway\TableGatewayInterface;
use RuntimeException;

class TaskTable
{
private $tableGateway;

public function __construct(TableGatewayInterface $tableGateway)
{
$this->tableGateway = $tableGateway;
}

public function fetchAll()
{
return $this->tableGateway->select();
}

public function getTask($id)
{
$id = (int) $id;
$rowset = $this->tableGateway->select(['id' => $id]);
$row = $rowset->current();

if (!$row) {
throw new RuntimeException(sprintf(
'Could not find row with identifier %d',
$id
));
}

return $row;
}

public function saveTask(Task $task)
{
$data = [
'title' => $task->title,
'description' => $task->description,
'status' => $task->status,
'priority' => $task->priority,
];

$id = (int) $task->id;

if ($id === 0) {
$this->tableGateway->insert($data);
return;
}

if (!$this->getTask($id)) {
throw new RuntimeException(sprintf(
'Cannot update task with identifier %d; does not exist',
$id
));
}

$this->tableGateway->update($data, ['id' => $id]);
}

public function deleteTask($id)
{
$this->tableGateway->delete(['id' => (int) $id]);
}
}

7. Create Task Form​

Create module/Todo/src/Form/TaskForm.php:

php
<?php

namespace Todo\Form;

use Laminas\Form\Form;
use Laminas\Form\Element;
use Laminas\InputFilter\InputFilterProviderInterface;

class TaskForm extends Form implements InputFilterProviderInterface
{
public function __construct($name = null)
{
parent::__construct('task-form');

$this->add([
'name' => 'id',
'type' => Element\Hidden::class,
]);

$this->add([
'name' => 'title',
'type' => Element\Text::class,
'options' => [
'label' => 'Title',
],
'attributes' => [
'id' => 'title',
'class' => 'form-control',
'required' => true,
],
]);

$this->add([
'name' => 'description',
'type' => Element\Textarea::class,
'options' => [
'label' => 'Description',
],
'attributes' => [
'id' => 'description',
'class' => 'form-control',
'rows' => 4,
],
]);

$this->add([
'name' => 'status',
'type' => Element\Select::class,
'options' => [
'label' => 'Status',
'value_options' => [
'pending' => 'Pending',
'completed' => 'Completed',
],
],
'attributes' => [
'id' => 'status',
'class' => 'form-control',
],
]);

$this->add([
'name' => 'priority',
'type' => Element\Select::class,
'options' => [
'label' => 'Priority',
'value_options' => [
'low' => 'Low',
'medium' => 'Medium',
'high' => 'High',
],
],
'attributes' => [
'id' => 'priority',
'class' => 'form-control',
],
]);

$this->add([
'name' => 'submit',
'type' => Element\Submit::class,
'attributes' => [
'value' => 'Save Task',
'class' => 'btn btn-primary',
],
]);
}

public function getInputFilterSpecification()
{
return [
'title' => [
'required' => true,
'filters' => [
['name' => 'StripTags'],
['name' => 'StringTrim'],
],
'validators' => [
[
'name' => 'StringLength',
'options' => [
'min' => 3,
'max' => 100,
],
],
],
],
'description' => [
'required' => false,
'filters' => [
['name' => 'StripTags'],


If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)