Getting started
Introduction
Phalcon is a high-performance PHP framework delivered as a C extension. Requires PHP 8.1+.
Installation
# Install via PECL
pecl install phalcon
# Enable extension
echo "extension=phalcon.so" > /etc/php/8.1/mods-available/phalcon.ini
phpenmod phalcon
# Verify installation
php -m | grep phalcon
Quick Example
<?php
use Phalcon\Mvc\Application;
use Phalcon\Di\FactoryDefault;
$container = new FactoryDefault();
$application = new Application($container);
echo $application->handle($_SERVER["REQUEST_URI"])->getContent();
Directory Structure
myapp/
├── app/
│ ├── controllers/
│ ├── models/
│ ├── views/
│ └── config/
└── public/
└── index.php
Routing
Basic Routes
<?php
use Phalcon\Mvc\Router;
$router = new Router();
// Simple route
$router->add(
'/products',
[
'controller' => 'products',
'action' => 'index',
]
);
// Route with parameters
$router->add(
'/products/{id}',
[
'controller' => 'products',
'action' => 'show',
]
);
Named Parameters
<?php
// Integer constraint
$router->add(
'/posts/{year:[0-9]{4}}/{title:[a-z\-]+}',
[
'controller' => 'posts',
'action' => 'show',
]
);
// Multiple parameters
$router->add(
'/users/{id:[0-9]+}/edit',
[
'controller' => 'users',
'action' => 'edit',
]
);
HTTP Methods
<?php
// GET only
$router->addGet('/products', [
'controller' => 'products',
'action' => 'index',
]);
// POST only
$router->addPost('/products', [
'controller' => 'products',
'action' => 'create',
]);
// Multiple methods
$router->add('/products', [
'controller' => 'products',
'action' => 'save',
])->via(['POST', 'PUT']);
Route Groups
<?php
use Phalcon\Mvc\Router\Group;
$posts = new Group([
'controller' => 'posts',
]);
$posts->setPrefix('/posts');
$posts->add('/', ['action' => 'index']);
$posts->add('/{id:[0-9]+}', ['action' => 'show']);
$posts->add('/create', ['action' => 'create']);
$router->mount($posts);
Named Routes
<?php
// Define named route
$router->add(
'/posts/{id}',
[
'controller' => 'posts',
'action' => 'show',
]
)->setName('show-post');
// Generate URL
$url = $this->url->get([
'for' => 'show-post',
'id' => 123,
]);
// Result: /posts/123
Controllers
Basic Controller
<?php
use Phalcon\Mvc\Controller;
class ProductsController extends Controller
{
public function indexAction()
{
// List products
}
public function showAction()
{
$id = $this->dispatcher->getParam('id');
$product = Products::findFirst($id);
$this->view->product = $product;
}
}
Request Parameters
<?php
class PostsController extends Controller
{
public function saveAction()
{
// Get route parameter
$id = $this->dispatcher->getParam('id');
// Get POST data
$title = $this->request->getPost('title');
// Get query parameter
$page = $this->request->getQuery('page', 'int', 1);
// Check HTTP method
if ($this->request->isPost()) {
// Handle POST
}
}
}
Response
<?php
class ApiController extends Controller
{
public function jsonAction()
{
$this->response->setJsonContent([
'status' => 'success',
'data' => $data,
]);
return $this->response;
}
public function redirectAction()
{
return $this->response->redirect('products/index');
}
}
View Control
<?php
class UsersController extends Controller
{
public function listAction()
{
// Disable view
$this->view->disable();
// Change view
$this->view->pick('users/custom');
// Pass variables
$this->view->users = Users::find();
$this->view->setVar('total', $users->count());
}
}
Models (ORM)
Basic Model
<?php
use Phalcon\Mvc\Model;
class Products extends Model
{
public $id;
public $name;
public $price;
public $active;
public function initialize()
{
$this->setSource('products');
}
}
Find Operations
<?php
// Find all
$products = Products::find();
// Find with conditions
$products = Products::find([
'conditions' => 'price > :price:',
'bind' => ['price' => 100],
'order' => 'name DESC',
'limit' => 10,
]);
// Find first
$product = Products::findFirst(123);
// Find first with conditions
$product = Products::findFirst([
'conditions' => 'name = :name:',
'bind' => ['name' => 'Laptop'],
]);
CRUD Operations
<?php
// Create
$product = new Products();
$product->name = 'Robot';
$product->price = 1999;
$product->save();
// Update
$product = Products::findFirst(123);
$product->price = 2499;
$product->update();
// Delete
$product = Products::findFirst(123);
$product->delete();
// Save (create or update)
$product->save();
Relationships
<?php
class Products extends Model
{
public function initialize()
{
// One-to-Many
$this->hasMany(
'id',
Reviews::class,
'product_id',
['alias' => 'reviews']
);
// Many-to-One
$this->belongsTo(
'category_id',
Categories::class,
'id',
['alias' => 'category']
);
// Many-to-Many
$this->hasManyToMany(
'id',
ProductsTags::class,
'product_id',
'tag_id',
Tags::class,
'id',
['alias' => 'tags']
);
}
}
Using Relationships
<?php
// Access related data
$product = Products::findFirst(123);
$reviews = $product->reviews;
$category = $product->category;
$tags = $product->tags;
// Eager loading
$products = Products::find([
'conditions' => 'price > 100',
])->load(['category', 'reviews']);
foreach ($products as $product) {
echo $product->category->name;
}
Model Events
<?php
class Products extends Model
{
public function beforeValidationOnCreate()
{
$this->created_at = date('Y-m-d H:i:s');
}
public function afterSave()
{
// Log activity
}
public function validation()
{
if ($this->price < 0) {
$this->appendMessage(
new Message('Price cannot be negative')
);
return false;
}
return true;
}
}
PHQL Queries
Basic Queries
<?php
use Phalcon\Mvc\Model\Query;
// SELECT
$phql = "SELECT * FROM Products WHERE price > :price:";
$query = $this->modelsManager->createQuery($phql);
$products = $query->execute([
'price' => 100,
]);
// INSERT
$phql = "INSERT INTO Products (name, price) VALUES (:name:, :price:)";
$query = $this->modelsManager->createQuery($phql);
$query->execute([
'name' => 'Robot',
'price' => 1999,
]);
Joins
<?php
$phql = "SELECT p.*, c.name as category_name
FROM Products p
JOIN Categories c ON p.category_id = c.id
WHERE p.price > :price:";
$products = $this->modelsManager->createQuery($phql)
->execute(['price' => 100]);
Aggregations
<?php
// Count
$phql = "SELECT COUNT(*) as total FROM Products";
$result = $this->modelsManager->createQuery($phql)->execute();
$total = $result->getFirst()->total;
// Group by
$phql = "SELECT category_id, COUNT(*) as total, AVG(price) as avg_price
FROM Products
GROUP BY category_id";
$stats = $this->modelsManager->createQuery($phql)->execute();
Query Builder
<?php
$products = $this->modelsManager->createBuilder()
->from(Products::class)
->where('price > :price:', ['price' => 100])
->orderBy('name ASC')
->limit(10)
->getQuery()
->execute();
// Join
$products = $this->modelsManager->createBuilder()
->addFrom(Products::class, 'p')
->join(Categories::class, 'p.category_id = c.id', 'c')
->where('p.active = 1')
->getQuery()
->execute();
Volt Templates
Basic Syntax
{# Comment #}
{# Variables #}
{{ product.name }}
{{ product.price }}
{# Expressions #}
{{ 1 + 1 }}
{{ 'Hello ' ~ name }}
{# Array access #}
{{ products[0] }}
{{ user['email'] }}
Control Structures
{# If statement #}
{% if product.active %}
<span>Active</span>
{% else %}
<span>Inactive</span>
{% endif %}
{# For loop #}
{% for product in products %}
<div>{{ product.name }}</div>
{% else %}
<div>No products found</div>
{% endfor %}
{# Loop variables #}
{% for item in items %}
{{ loop.index }}: {{ item.name }}
{% if loop.first %}First{% endif %}
{% if loop.last %}Last{% endif %}
{% endfor %}
Filters
{# Escape HTML #}
{{ content | e }}
{# Strip tags #}
{{ content | striptags }}
{# Uppercase/lowercase #}
{{ name | upper }}
{{ name | lower }}
{# Trim #}
{{ text | trim }}
{# Length #}
{{ items | length }}
{# Date format #}
{{ created_at | date('Y-m-d') }}
{# Default value #}
{{ title | default('Untitled') }}
Template Inheritance
{# base.volt #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Default Title{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
{# products.volt #}
{% extends "base.volt" %}
{% block title %}Products{% endblock %}
{% block content %}
<h1>Products</h1>
{% for product in products %}
<div>{{ product.name }}</div>
{% endfor %}
{% endblock %}
Includes
{# Include partial #}
{% include "partials/header.volt" %}
{# Include with variables #}
{% include "partials/product.volt" with ['product': item] %}
Macros
{# Define macro #}
{% macro input(name, value, type) %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{# Use macro #}
{{ input('email', user.email, 'email') }}
Dependency Injection
Container Setup
<?php
use Phalcon\Di\FactoryDefault;
$container = new FactoryDefault();
// Set service
$container->set('config', function () {
return include 'config/config.php';
});
// Shared service (singleton)
$container->setShared('db', function () use ($container) {
return new \Phalcon\Db\Adapter\Pdo\Mysql([
'host' => 'localhost',
'username' => 'root',
'password' => 'secret',
'dbname' => 'myapp',
]);
});
Service Registration
<?php
// Database
$container->setShared('db', function () {
return new \Phalcon\Db\Adapter\Pdo\Mysql([
'host' => 'localhost',
'username' => 'user',
'password' => 'pass',
'dbname' => 'database',
]);
});
// View
$container->setShared('view', function () {
$view = new \Phalcon\Mvc\View();
$view->setViewsDir('../app/views/');
return $view;
});
// URL
$container->setShared('url', function () {
$url = new \Phalcon\Url();
$url->setBaseUri('/');
return $url;
});
Volt Service
<?php
$container->setShared('view', function () {
$view = new \Phalcon\Mvc\View();
$view->setViewsDir('../app/views/');
$view->registerEngines([
'.volt' => function ($view) {
$volt = new \Phalcon\Mvc\View\Engine\Volt($view);
$volt->setOptions([
'path' => '../app/cache/volt/',
'separator' => '_',
]);
return $volt;
},
]);
return $view;
});
Accessing Services
<?php
// In controllers
$this->db->query('SELECT * FROM products');
$this->view->setVar('title', 'Home');
$this->request->getPost('name');
// Using container
$db = $container->get('db');
$config = $container->getShared('config');
// Array syntax
$db = $container['db'];
Transactions
Manual Transactions
<?php
use Phalcon\Mvc\Model\Transaction\Manager;
$manager = new Manager();
$transaction = $manager->get();
$product = new Products();
$product->setTransaction($transaction);
$product->name = 'Robot';
if ($product->save() === false) {
$transaction->rollback('Cannot save product');
}
$review = new Reviews();
$review->setTransaction($transaction);
$review->product_id = $product->id;
if ($review->save() === false) {
$transaction->rollback('Cannot save review');
}
$transaction->commit();
Automatic Transactions
<?php
use Phalcon\Mvc\Model\Transaction\Manager;
$manager = new Manager();
try {
$transaction = $manager->get();
$product = new Products();
$product->setTransaction($transaction);
$product->save();
$category = new Categories();
$category->setTransaction($transaction);
$category->save();
$transaction->commit();
} catch (\Exception $e) {
if (isset($transaction)) {
$transaction->rollback();
}
echo $e->getMessage();
}
Configuration
Bootstrap (index.php)
<?php
use Phalcon\Di\FactoryDefault;
use Phalcon\Mvc\Application;
define('BASE_PATH', dirname(__DIR__));
define('APP_PATH', BASE_PATH . '/app');
// DI Container
$container = new FactoryDefault();
// Register services
require APP_PATH . '/config/services.php';
// Handle request
$application = new Application($container);
try {
$response = $application->handle(
$_SERVER["REQUEST_URI"]
);
$response->send();
} catch (\Exception $e) {
echo $e->getMessage();
}
Config File
<?php
// config/config.php
return [
'database' => [
'adapter' => 'Mysql',
'host' => 'localhost',
'username' => 'root',
'password' => 'secret',
'dbname' => 'myapp',
'charset' => 'utf8',
],
'application' => [
'controllersDir' => APP_PATH . '/controllers/',
'modelsDir' => APP_PATH . '/models/',
'viewsDir' => APP_PATH . '/views/',
'cacheDir' => BASE_PATH . '/cache/',
'baseUri' => '/',
],
];
Services Registration
<?php
// config/services.php
// Config
$container->setShared('config', function () {
return include APP_PATH . '/config/config.php';
});
// Database
$container->setShared('db', function () {
$config = $this->getConfig();
return new \Phalcon\Db\Adapter\Pdo\Mysql([
'host' => $config->database->host,
'username' => $config->database->username,
'password' => $config->database->password,
'dbname' => $config->database->dbname,
'charset' => $config->database->charset,
]);
});
// Router
$container->setShared('router', function () {
$router = new \Phalcon\Mvc\Router();
require APP_PATH . '/config/routes.php';
return $router;
});
Multi-Module Structure
Application Setup
<?php
use Phalcon\Mvc\Application;
$application = new Application($container);
$application->registerModules([
'frontend' => [
'className' => 'App\Frontend\Module',
'path' => '../apps/frontend/Module.php',
],
'backend' => [
'className' => 'App\Backend\Module',
'path' => '../apps/backend/Module.php',
],
]);
Module Class
<?php
namespace App\Frontend;
use Phalcon\Mvc\ModuleDefinitionInterface;
class Module implements ModuleDefinitionInterface
{
public function registerAutoloaders($container = null)
{
// Register namespaces
}
public function registerServices($container)
{
// Module-specific services
$container->set('view', function () {
$view = new \Phalcon\Mvc\View();
$view->setViewsDir('../apps/frontend/views/');
return $view;
});
}
}
Database Support
MySQL
<?php
$container->setShared('db', function () {
return new \Phalcon\Db\Adapter\Pdo\Mysql([
'host' => 'localhost',
'username' => 'root',
'password' => 'secret',
'dbname' => 'myapp',
]);
});
PostgreSQL
<?php
$container->setShared('db', function () {
return new \Phalcon\Db\Adapter\Pdo\Postgresql([
'host' => 'localhost',
'username' => 'postgres',
'password' => 'secret',
'dbname' => 'myapp',
]);
});
SQLite
<?php
$container->setShared('db', function () {
return new \Phalcon\Db\Adapter\Pdo\Sqlite([
'dbname' => '/path/to/database.sqlite',
]);
});
Validation
Built-in Validators
<?php
use Phalcon\Validation;
use Phalcon\Validation\Validator\PresenceOf;
use Phalcon\Validation\Validator\Email;
use Phalcon\Validation\Validator\Numericality;
$validation = new Validation();
$validation->add(
'name',
new PresenceOf(['message' => 'Name is required'])
);
$validation->add(
'email',
new Email(['message' => 'Invalid email'])
);
$validation->add(
'price',
new Numericality(['message' => 'Price must be numeric'])
);
$messages = $validation->validate($_POST);
if (count($messages)) {
foreach ($messages as $message) {
echo $message;
}
}
Model Validation
<?php
use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Email;
class Users extends Model
{
public function validation()
{
$validator = new Validation();
$validator->add(
'email',
new Email(['message' => 'Invalid email'])
);
return $this->validate($validator);
}
}
Caching
File Cache
<?php
use Phalcon\Cache\Cache;
use Phalcon\Cache\AdapterFactory;
use Phalcon\Storage\SerializerFactory;
$serializerFactory = new SerializerFactory();
$adapterFactory = new AdapterFactory($serializerFactory);
$adapter = $adapterFactory->newInstance('apcu');
$cache = new Cache($adapter);
// Set cache
$cache->set('products', $products, 3600);
// Get cache
$products = $cache->get('products');
// Delete cache
$cache->delete('products');
Security
Password Hashing
<?php
use Phalcon\Security;
$security = new Security();
// Hash password
$hash = $security->hash('password123');
// Verify password
if ($security->checkHash('password123', $hash)) {
// Valid password
}
CSRF Protection
<?php
// In form
<input type="hidden" name="<?= $security->getTokenKey() ?>"
value="<?= $security->getToken() ?>">
// Validate
if ($security->checkToken()) {
// Valid token
}
Full MVC Example
Controller
<?php
namespace App\Controllers;
use Phalcon\Mvc\Controller;
use App\Models\Products;
class ProductsController extends Controller
{
public function indexAction()
{
$this->view->products = Products::find([
'order' => 'name ASC',
]);
}
public function showAction($id)
{
$product = Products::findFirst($id);
if (!$product) {
$this->response->redirect('products');
return;
}
$this->view->product = $product;
}
public function createAction()
{
if ($this->request->isPost()) {
$product = new Products();
$product->name = $this->request->getPost('name');
$product->price = $this->request->getPost('price');
if ($product->save()) {
$this->flash->success('Product created');
$this->response->redirect('products');
} else {
foreach ($product->getMessages() as $message) {
$this->flash->error($message);
}
}
}
}
}
Model with Relationships
<?php
namespace App\Models;
use Phalcon\Mvc\Model;
class Products extends Model
{
public $id;
public $category_id;
public $name;
public $price;
public $created_at;
public function initialize()
{
$this->setSource('products');
$this->belongsTo(
'category_id',
Categories::class,
'id',
['alias' => 'category']
);
$this->hasMany(
'id',
Reviews::class,
'product_id',
['alias' => 'reviews']
);
}
public function beforeValidationOnCreate()
{
$this->created_at = date('Y-m-d H:i:s');
}
}
Volt Template
{# views/products/index.volt #}
{% extends "layouts/main.volt" %}
{% block title %}Products{% endblock %}
{% block content %}
<h1>Products</h1>
<a href="{{ url('products/create') }}">Create Product</a>
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr>
<td>{{ product.name }}</td>
<td>${{ product.price }}</td>
<td>{{ product.category.name }}</td>
<td>
<a href="{{ url('products/show/' ~ product.id) }}">View</a>
<a href="{{ url('products/edit/' ~ product.id) }}">Edit</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="4">No products found</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
Also see
- Phalcon Official Documentation - Comprehensive framework guide
- Phalcon GitHub Repository - Source code and issue tracker
- Phalcon Forum - Community support
- Phalcon DevTools - CLI scaffolding tools