Розробка бекенду сайту на нативному PHP

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка бекенду сайту на нативному PHP
Середня
~3-5 робочих днів
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка веб-бекенду на нативному PHP

Нативний PHP без фреймворку — не архаїка. Це усвідомлений вибір, коли потрібний максимальний контроль, мінімальний overhead або коли проект уже існує і менти стек недоцільна. PHP 8.2+ з типами, атрибутами, readonly-класами та волокнами — це зовсім інша мова порівняно з PHP 5.

Нативний PHP виправданий для: невеликих API з передбачуваною функціональністю, shared hosting без можливості встановлення фреймворків, міграції legacy-проектів частинами, мікросервісів, критичних за продуктивністю.

Точка Входу та Маршрутизація

Центральна схема — єдина точка входу (index.php) з роутером:

<?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();

// Реєстрація маршрутів
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']);

Роутер

<?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
        foreach ($middlewares as $middleware) {
            $result = app($middleware)->handle($request);
            if ($result instanceof Response) return $result;
        }

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

fastroute/fastroute — найкращий PHP-роутер за продуктивністю, використовується всередині Slim та інших фреймворків.

PDO та Робота з БД

<?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;
    }
}

// Мінімальний 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();
    }
}

Валідація

<?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} обов'язкове")
                : null,
            'min' => is_string($value) && strlen($value) < (int) $param
                ? $this->addError($field, 'min', "Мінімум {$param} символів")
                : null,
            'max' => is_string($value) && strlen($value) > (int) $param
                ? $this->addError($field, 'max', "Максимум {$param} символів")
                : null,
            'numeric' => $value !== null && !is_numeric($value)
                ? $this->addError($field, 'numeric', "Поле {$field} має бути числом")
                : null,
            'email' => $value && !filter_var($value, FILTER_VALIDATE_EMAIL)
                ? $this->addError($field, 'email', 'Неправильний 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 без Бібліотек для Простих Випадків

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}";
}

// Але краще використовувати firebase/php-jwt для production

Коли Нативний PHP Закінчується

Нативний підхід вимагає написання інфраструктурного коду самостійно — роутер, DI-контейнер, валідатор, міграції. На практиці це означає або використання composer-бібліотек (fastroute, php-di, phinx), або написання свого. В обох випадках при зростанні проекту починає працювати фреймворк — або чужий (Symfony Components), або свій неповний.

Межа рентабельності нативного PHP — проекти до 15–20 endpoints з простою логікою. Далі фреймворк економить час.

Терміни Розробки

  • Базова інфраструктура: роутер, DI, Request/Response — 3–7 днів
  • Моделі + PDO + Query Builder — 3–5 днів
  • Auth + JWT — 2–3 дні
  • Бізнес-логіка — 1–3 тижні

Невеликий API або віджет для існуючого сайту: 2–5 тижнів.