NexusCS

Phalcon

PHP
Quick reference for Phalcon - a high-performance PHP framework delivered as a C extension for maximum speed with minimal overhead.
php
framework
mvc
c-extension

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