Website Backend Development with Native PHP

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Website Backend Development with Native PHP
Medium
~3-5 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Website Backend Development with Native PHP

Native PHP without a framework is not archaic. It's a conscious choice when maximum control, minimal overhead is needed, or when a project already exists and changing the stack isn't feasible. PHP 8.2+ with types, attributes, readonly classes, and fibers is a completely different language compared to PHP 5.

Native PHP is justified for: small APIs with predictable functionality, shared hosting without framework installation capability, legacy project migrations in parts, microservices critical for performance.

Entry Point and Routing

Central pattern — single entry point (index.php) with a router:

<?php
// public/index.php

declare(strict_types=1);

require_once __DIR__ . '/../vendor/autoload.php';

use App\Core\Router;
use App\Core\Request;
use App\Core\Response;
use App\Middleware\CorsMiddleware;
use App\Middleware\AuthMiddleware;

$request = Request::fromGlobals();
$router  = new Router();

// Route registration
require __DIR__ . '/../routes/api.php';

// Middleware pipeline
$middlewares = [new CorsMiddleware(), new AuthMiddleware()];
$response    = (new Pipeline($middlewares))->handle($request, fn($req) => $router->dispatch($req));
$response->send();
<?php
// routes/api.php

use App\Controllers\ProductController;
use App\Controllers\AuthController;

$router->get('/api/v1/products',         [ProductController::class, 'index']);
$router->get('/api/v1/products/{id:\d+}', [ProductController::class, 'show']);
$router->post('/api/v1/products',         [ProductController::class, 'create'], ['auth', 'admin']);
$router->delete('/api/v1/products/{id}',  [ProductController::class, 'destroy'], ['auth', 'admin']);

$router->post('/api/v1/auth/login',   [AuthController::class, 'login']);
$router->post('/api/v1/auth/refresh', [AuthController::class, 'refresh']);

Router

<?php
namespace App\Core;

use FastRoute\Dispatcher;
use FastRoute\RouteCollector;
use function FastRoute\simpleDispatcher;

class Router
{
    private array $routes = [];

    public function get(string $pattern, array $handler, array $middlewares = []): void
    {
        $this->routes[] = ['GET', $pattern, $handler, $middlewares];
    }

    public function post(string $pattern, array $handler, array $middlewares = []): void
    {
        $this->routes[] = ['POST', $pattern, $handler, $middlewares];
    }

    public function dispatch(Request $request): Response
    {
        $dispatcher = simpleDispatcher(function (RouteCollector $r) {
            foreach ($this->routes as [$method, $pattern, $handler, $middlewares]) {
                $r->addRoute($method, $pattern, [$handler, $middlewares]);
            }
        });

        $info = $dispatcher->dispatch($request->method(), $request->path());

        return match ($info[0]) {
            Dispatcher::FOUND => $this->handleFound($info[1], $info[2], $request),
            Dispatcher::NOT_FOUND => Response::json(['error' => 'Not Found'], 404),
            Dispatcher::METHOD_NOT_ALLOWED => Response::json(['error' => 'Method Not Allowed'], 405),
        };
    }

    private function handleFound(array $routeInfo, array $params, Request $request): Response
    {
        [$handler, $middlewares] = $routeInfo;
        [$class, $method] = $handler;

        // Middleware check
        foreach ($middlewares as $middleware) {
            $result = app($middleware)->handle($request);
            if ($result instanceof Response) return $result;
        }

        return app($class)->$method($request, $params);
    }
}

fastroute/fastroute — the best PHP router by performance, used inside Slim and other frameworks.

PDO and Database Work

<?php
namespace App\Core;

use PDO;
use PDOStatement;

class Database
{
    private static ?PDO $instance = null;

    public static function getInstance(): PDO
    {
        if (self::$instance === null) {
            $dsn = sprintf('pgsql:host=%s;dbname=%s;port=%s',
                getenv('DB_HOST'), getenv('DB_NAME'), getenv('DB_PORT') ?: '5432'
            );
            self::$instance = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES   => false,
            ]);
        }
        return self::$instance;
    }
}

// Minimal Query Builder
class QueryBuilder
{
    private PDO $pdo;
    private string $table;
    private array $conditions = [];
    private array $bindings   = [];
    private ?int $limit       = null;
    private ?int $offset      = null;

    public function __construct(string $table)
    {
        $this->pdo   = Database::getInstance();
        $this->table = $table;
    }

    public function where(string $column, mixed $value): static
    {
        $placeholder = ':' . $column . count($this->bindings);
        $this->conditions[] = "{$column} = {$placeholder}";
        $this->bindings[$placeholder] = $value;
        return $this;
    }

    public function limit(int $limit): static { $this->limit = $limit; return $this; }
    public function offset(int $offset): static { $this->offset = $offset; return $this; }

    public function get(): array
    {
        $sql  = "SELECT * FROM {$this->table}";
        if ($this->conditions) {
            $sql .= ' WHERE ' . implode(' AND ', $this->conditions);
        }
        if ($this->limit !== null) $sql .= " LIMIT {$this->limit}";
        if ($this->offset !== null) $sql .= " OFFSET {$this->offset}";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($this->bindings);
        return $stmt->fetchAll();
    }

    public function insert(array $data): int
    {
        $columns     = implode(', ', array_keys($data));
        $placeholders = implode(', ', array_map(fn($k) => ":{$k}", array_keys($data)));
        $stmt        = $this->pdo->prepare("INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})");
        $stmt->execute($data);
        return (int) $this->pdo->lastInsertId();
    }
}

Validation

<?php
namespace App\Core;

class Validator
{
    private array $errors = [];

    public function validate(array $data, array $rules): bool
    {
        $this->errors = [];
        foreach ($rules as $field => $fieldRules) {
            foreach (explode('|', $fieldRules) as $rule) {
                $this->applyRule($data, $field, $rule);
            }
        }
        return empty($this->errors);
    }

    private function applyRule(array $data, string $field, string $rule): void
    {
        $value = $data[$field] ?? null;
        [$ruleName, $param] = array_pad(explode(':', $rule, 2), 2, null);

        match ($ruleName) {
            'required' => !isset($data[$field]) || $data[$field] === ''
                ? $this->addError($field, 'required', "Field {$field} is required")
                : null,
            'min' => is_string($value) && strlen($value) < (int) $param
                ? $this->addError($field, 'min', "Minimum {$param} characters")
                : null,
            'max' => is_string($value) && strlen($value) > (int) $param
                ? $this->addError($field, 'max', "Maximum {$param} characters")
                : null,
            'numeric' => $value !== null && !is_numeric($value)
                ? $this->addError($field, 'numeric', "Field {$field} must be a number")
                : null,
            'email' => $value && !filter_var($value, FILTER_VALIDATE_EMAIL)
                ? $this->addError($field, 'email', 'Invalid email')
                : null,
            default => null
        };
    }

    private function addError(string $field, string $rule, string $message): void
    {
        $this->errors[$field][$rule] = $message;
    }

    public function getErrors(): array { return $this->errors; }
}

JWT without Libraries for Simple Cases

function createJwt(array $payload, string $secret, int $ttl = 3600): string
{
    $header   = base64_url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
    $payload  = base64_url_encode(json_encode(array_merge($payload, ['exp' => time() + $ttl])));
    $sig      = base64_url_encode(hash_hmac('sha256', "{$header}.{$payload}", $secret, true));
    return "{$header}.{$payload}.{$sig}";
}

// But better to use firebase/php-jwt for production

When Native PHP Ends

Native approach requires writing infrastructure code yourself — router, DI container, validator, migrations. In practice this means either using composer libraries (fastroute, php-di, phinx) or writing your own. In both cases, as the project grows, a framework starts working — either external (Symfony Components) or your incomplete one.

Profitability boundary for native PHP — projects up to 15–20 endpoints with simple logic. Beyond that, a framework saves time.

Development Timeline

  • Basic infrastructure: router, DI, Request/Response — 3–7 days
  • Models + PDO + Query Builder — 3–5 days
  • Auth + JWT — 2–3 days
  • Business logic — 1–3 weeks

Small API or widget for existing website: 2–5 weeks.