ДокументацияFullstack-фреймворки
От Laravel до Nexy
В Nexy маршрут Laravel и его метод контроллера становятся одним файлом в src/routes/: представление Blade становится страницей .nexy, $request->validate - моделью Pydantic, а Eloquent - выбранным вами ORM, например SQLModel.
В каждом разделе ниже показаны особенности Laravel, а затем то же самое в Nexy.
Маршрут
use Illuminate\Support\Facades\Route; Route::get('/hello', fn () => ['message' => 'Hello']);Файла маршрутов нет: путь к файлу - это URL-адрес, а имя функции - это метод.
async def GET(): return {"message": "Hello"}Параметры маршрута и where
Route::get('/users/{id}', fn (int $id) => ['id' => $id])->whereNumber('id');{id} - это сегмент [id], а аннотация int - это ограничение: /api/users/abc отвечает 404.
async def GET(id: int): return {"id": id}Необязательные {slug?} или {path} соответствующие косые черты (->where('path', '.*')) - это универсальный вариант [...path].py.
Контроллеры
class NoteController extends Controller{ public function show(int $id) { return ['id' => $id]; } public function destroy(int $id) { return ['deleted' => $id]; }}Route::get('/notes/{id}', [NoteController::class, 'show']);Route::delete('/notes/{id}', [NoteController::class, 'destroy']);Методы контроллера - это функции файла, названного в честь глагола:
async def GET(id: int): return {"id": id} async def DELETE(id: int): return {"deleted": id}Для классов с внедренными сервисами, например, в контроллере Laravel, см. модульный маршрутизатор.
Строки запроса
public function __invoke(Request $request){ return ['q' => $request->query('q', ''), 'page' => (int) $request->query('page', 1)];}async def GET(q: str = "", page: int = 1): return {"q": q, "page": page}Проверка и запросы форм
public function store(Request $request){ $data = $request->validate([ 'name' => 'required|string|max:100', 'price' => 'required|numeric|gt:0', ]); return response()->json($data, 201);}Правила представляют собой модель Pydantic. Тело, которое нарушает одно, отвечает 422 полем и правилом, как это делает Laravel:
from pydantic import BaseModel, Field from nexy.http import JSONResponse class NewItem(BaseModel): name: str = Field(max_length=100) price: float = Field(gt=0) async def POST(item: NewItem): return JSONResponse(item.model_dump(), status_code=201)Группы маршрутов с префиксом и промежуточным программным обеспечением
Route::prefix('admin')->middleware('admin')->group(function () { Route::get('/stats', fn () => ['users' => 3]);});public function handle(Request $request, Closure $next){ abort_unless($request->header('X-Role') === 'admin', 403, 'Admins only'); return $next($request);}Папка является префиксом, а ее dependencies.py - промежуточным программным обеспечением группы:
async def GET(): return {"users": 3}from nexy.http import Header, HTTPException def require_admin(x_role: str = Header("")) -> None: if x_role != "admin": raise HTTPException(status_code=403, detail="Admins only") dependencies = [require_admin]Глобальное промежуточное программное обеспечение
public function handle(Request $request, Closure $next){ $response = $next($request); $response->headers->set('X-App', 'demo'); return $response;}middleware.py в корне маршрута оборачивает каждый запрос:
from nexy.http import Request async def middleware(request: Request, call_next): response = await call_next(request) response.headers["x-app"] = "demo" return responseВиды лезвий
Route::get('/users', fn () => view('users', ['users' => [['name' => 'Ada'], ['name' => 'Alan']]]));
@extends('layouts.app') @section('content')<ul> @foreach ($users as $user) <li>{{ $user['name'] }}</li> @endforeach</ul>@endsectionМаршрут и представление представляют собой один файл .nexy:
users = [{"name": "Ada"}, {"name": "Alan"}]<ul> {% for user in users %} <li>{{ user.name }}</li> {% endfor %}</ul>| Лезвие | Jinja2 в .nexy |
|---|---|
{{ $name }} (сбежал) |
{{ name | e }}: .nexy по умолчанию не экранируется |
{!! $html !!} |
{{ html | safe }} |
@if ($user) ... @endif |
{% if user %} ... {% endif %} |
@foreach ($items as $item) |
{% for item in items %} |
@php ... @endphp |
Python во вступительной части |
Макеты и @yield
<!DOCTYPE html><html> <body>@yield('content')</body></html>layout.nexy оборачивает каждую страницу своей папки, без @extends на страницах:
children: prop[NexyElement]<!DOCTYPE html><html> <body>{{ children | safe }}</body></html>Компоненты лезвия
@props(['title'])<article> <h2>{{ $title }}</h2> {{ $slot }}</article><x-card title="Latest"><p>This week</p></x-card>
title: prop[str]<article> <h2>{{ title }}</h2> <Slot /></article>from "@/components/Card.nexy" import Card<Card title="Latest"> <p>This week</p></Card>@ - это src/: nx new записывает useAliases = {"@": "src"} в nexyconfig.py.
Формы, @csrf и перенаправления
<form method="POST" action="{{ route('notes.store') }}"> @csrf <input name="title"> <button type="submit">Add</button></form>public function store(Request $request){ Note::create($request->validate(['title' => 'required'])); return back();}Действие - это метод контроллера, которому отправляется форма. Его параметрами являются поля, и в случае успеха посетитель переходит к back() с сообщением 303:
from nexy.decorators import Action NOTES: list[str] = [] @Actionasync def add_note(title: str) -> None: NOTES.append(title)from src.actions.notes import NOTES<form method="post" action="{{ __actions_path['add_note'] }}"> <input name="title"> <button type="submit">Add</button></form><ul> {% for note in NOTES %} <li>{{ note }}</li> {% endfor %}</ul>Красноречивый и миграции
class Post extends Model{ protected $fillable = ['title', 'published'];} $posts = Post::where('published', true)->get();У Nexy нет собственного ORM. nx new может поддерживать SQLModel, SQLAlchemy или Tortoise-ORM; с SQLModel:
from sqlmodel import Field, SQLModel class Post(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) title: str published: bool = Falsefrom sqlmodel import Session, select from src.configs.db import enginefrom src.models.post_model import Post async def GET(): with Session(engine) as session: posts = session.exec(select(Post).where(Post.published)).all() return {"titles": [post.title for post in posts]}| Ремесленник | Некси |
|---|---|
php artisan make:migration |
nx migrate -m "add posts" |
php artisan migrate |
nx migrate --upgrade |
См. Базы данных.
abort(404) и просмотры ошибок
Route::get('/posts/{slug}', function (string $slug) { abort_unless(isset(POSTS[$slug]), 404); return view('post', ['post' => POSTS[$slug]]);});
notFound() - это abort(404), а notfound.nexy - это resources/views/errors/404.blade.php:
from nexy import notFound POSTS = {"hello": "Hello, world"} slug: prop[str]if slug not in POSTS: notFound()<h1>{{ POSTS[slug] }}</h1><h1>Not found</h1>Сессии
public function store(Request $request){ session(['user' => $request->input('name')]); return ['user' => session('user')];}from nexy.core.models import NexyConfigModel class NexyConfig(NexyConfigModel): useAliases = {"@": "src"} useSession = {"secret_key": "change-me"}from pydantic import BaseModel from nexy.http import Request class Login(BaseModel): name: str async def POST(request: Request, body: Login): request.session["user"] = body.name return {"user": body.name}Страница считывает сеанс с помощью useSession().
Очереди и dispatch
SendWelcomeEmail::dispatch($user); return response()->json(['queued' => true], 202);Для работы, которая может запуститься после ответа, достаточно фоновых задач FastAPI:
from nexy.http import BackgroundTasks, JSONResponse SENT: list[str] = [] def send_welcome_email(email: str) -> None: SENT.append(email) async def POST(email: str, tasks: BackgroundTasks): tasks.add_task(send_welcome_email, email) return JSONResponse({"queued": True}, status_code=202)Для повторных попыток и рабочих процессов на других машинах используйте очередь Python, например Celery, RQ или arq.
Кэширование страницы
Route::get('/blog', BlogController::class)->middleware('cache.headers:public;max_age=60');
revalidate = 60<h1>Blog</h1>Страница кэшируется для каждого URL-адреса и снова отображается в фоновом режиме через 60 секунд.
.env и config/
| Ларавел | Некси |
|---|---|
.env + config/database.php |
.env с DATABASE_URL, прочитано src/configs/db.py |
config/cors.php |
useCORS в nexyconfig.py |
APP_KEY для сеансов |
useSession = {"secret_key": ...} |
public/ |
public/, служил в / |
Livewire и инерция
Серверные компоненты Livewire сопоставляются с действиями, вызываемыми со страницы. Страницы Inertia React или Vue соответствуют маршрутам .tsx или .vue или островам внутри страницы .nexy. См. Действия и Острова.
Тесты
public function test_hello(): void{ $this->getJson('/api/hello')->assertExactJson(['message' => 'Hello']);}from fastapi.testclient import TestClient from nexy.app import app def test_hello(): assert TestClient(app).get("/api/hello").json() == {"message": "Hello"}Бег
| Ларавел | Некси |
|---|---|
laravel new |
nx new |
php artisan serve |
nx dev |
php artisan route:list |
/docs |
php artisan test |
nx test |
npm run build |
nx build |