# php-arcmvc
**Repository Path**: wfdaj/php-arcmvc
## Basic Information
- **Project Name**: php-arcmvc
- **Description**: 仓库路径即仓库访问 URL 地址,更改仓库路径将导致原克隆地址不可用
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-06-05
- **Last Updated**: 2026-06-18
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Arc
A lightweight, modern PHP MVC framework. Small core, batteries included, built for PHP 8.4+.
## Principles
- No hidden magic. Request flow is traceable from `public/index.php` to response.
- Decoupled core: uses a dedicated DI container for service management.
- Secure defaults: CSRF protection, XSS escaping, SQL injection prevention, security headers.
- Fast startup, low memory by default.
- One canonical way to do common things.
- First-party modules for the common website stack, each optional and independently replaceable.
## Requirements
- PHP 8.4+
- PDO extension (for database features)
- Composer
## Quick Start
```bash
# Create a new project (installs dependencies automatically)
arc new my-project
cd my-project
arc serve
```
Or add Arc to an existing project:
```bash
composer require andrewthecoder/arcmvc
cp -r vendor/andrewthecoder/arcmvc/skeleton/* .
```
Visit [http://localhost:8080](http://localhost:8080)
## Environment Setup
Copy `.env.example` to `.env` and adjust values:
```bash
cp .env.example .env
```
Arc includes a built-in `.env` loader. Load it early in your bootstrap:
```php
use Arc\Config\EnvLoader;
EnvLoader::load(__DIR__ . '/../.env');
```
Environment variables are available via `$_ENV` and `getenv()`. Existing env vars are never overwritten.
## Routing
Define routes in `routes/web.php`:
```php
$router->get('/', [HomeController::class, 'index']);
$router->get('/users/{id}', [UserController::class, 'show']);
$router->post('/users', [UserController::class, 'store']);
$router->put('/users/{id}', [UserController::class, 'update']);
$router->delete('/users/{id}', [UserController::class, 'destroy']);
```
Route groups with prefix and middleware:
```php
$router->group(['prefix' => 'admin', 'middleware' => [AuthMiddleware::class]], function ($router) {
$router->get('/dashboard', [AdminController::class, 'index']);
});
```
## Controllers
Controllers extend `Arc\Support\Controller` and receive the current request via `setRequest()`:
```php
use Arc\Support\Controller;
use Arc\Http\Request;
use Arc\Http\Response;
class UserController extends Controller
{
public function show(Request $request, string $id): Response
{
$user = User::find($id);
return $this->view('users.show', ['user' => $user]);
}
public function store(Request $request): Response
{
return $this->redirect('/users');
}
protected function back(): Response // Safe redirect, rejects external URLs
{
return parent::back();
}
}
```
Controllers are resolved through the DI container, enabling constructor injection.
## Views and Templates
Views live in `resources/views/` and use `.phtml` files:
```php
extend('main') ?>
section('title', 'Home Page') ?>
Page content here
```
Layouts use `yield()` for content:
```php
= $this->yield('title', 'Arc') ?>
= $this->yield('content') ?>
```
### XSS Escaping
Use `e()` to escape user-supplied data:
```php
= $this->e($userInput) ?>
```
Content and named sections yielded via `yield()` are **raw** by design (they contain trusted template HTML). Always escape user data with `e()`.
### CSRF Protection
Include a CSRF token in forms:
```php
```
The `CsrfMiddleware` validates the token automatically on POST, PUT, PATCH, and DELETE requests.
### Partials
```php
= $this->partial('shared._nav', ['label' => 'Home']) ?>
```
## Middleware
Immutable middleware examples
- Decorate responses immutably with withHeader() so no other layer sees mutations.
- Pass request-scoped data down the pipeline with Request::withAttribute().
Example: add security headers without mutating the original Response
```php
use Arc\Http\MiddlewareInterface;
use Arc\Http\Request;
use Arc\Http\Response;
final class SecurityHeadersMiddleware implements MiddlewareInterface
{
public function handle(Request $request, callable $next): Response
{
$response = $next($request);
return $response
->withHeader('X-Frame-Options', 'DENY')
->withHeader('X-Content-Type-Options', 'nosniff')
->withHeader('Referrer-Policy', 'no-referrer')
->withHeader('Permissions-Policy', 'camera=(), microphone=()');
}
}
```
Example: attach request-scoped data immutably
```php
final class AuthMiddleware implements MiddlewareInterface
{
public function handle(Request $request, callable $next): Response
{
$userId = $this->authenticate($request); // returns null or an ID
$requestWithUser = $request->withAttribute('user_id', $userId);
return $next($requestWithUser);
}
private function authenticate(Request $request): ?int { /* ... */ }
}
```
Register global middleware in your bootstrap:
```php
use Arc\Http\Middleware\SecurityMiddleware;
use Arc\Http\Middleware\CsrfMiddleware;
use Arc\Http\Middleware\RateLimitMiddleware;
$app->addMiddleware(SecurityMiddleware::class);
$app->addMiddleware(new CsrfMiddleware());
$app->addMiddleware(new RateLimitMiddleware(maxRequests: 60, windowSeconds: 60));
```
### Security Headers
`SecurityMiddleware` sets headers with configurable defaults:
```php
new SecurityMiddleware(
csp: "default-src 'self'; script-src 'self' cdn.example.com",
hsts: 'max-age=63072000; includeSubDomains; preload'
);
```
### CSRF
`CsrfMiddleware` uses the double-submit cookie pattern with a SameSite=Strict, HttpOnly cookie (marked `Secure` automatically on HTTPS requests). The token is attached to the request as the `_csrf_token` attribute and is automatically passed to views rendered via `Controller::view()`, so `csrfField()` works without manual wiring.
### Rate Limiting
`RateLimitMiddleware` tracks requests per client IP with configurable limits:
```php
new RateLimitMiddleware(maxRequests: 100, windowSeconds: 60);
```
Behind a reverse proxy, pass the proxy IPs as `trustedProxies` so the client is read from `X-Forwarded-For` (it is ignored from untrusted peers, preventing spoofing). For stricter per-route limits, supply a `keyResolver`:
```php
new RateLimitMiddleware(
maxRequests: 5,
windowSeconds: 60,
trustedProxies: ['10.0.0.1'],
keyResolver: fn ($request) => 'login:' . $request->ip(['10.0.0.1']),
);
```
The default in-memory store is per-process; for multi-process or distributed deployments, implement `RateLimitStoreInterface` with Redis or a database backend.
## HTTP Request & Response
- Request: use withAttribute(key, value) to return a cloned instance with a new attribute. setAttribute() still exists and mutates in place; prefer withAttribute() in middleware pipelines to avoid shared-reference surprises.
- Response: in addition to mutating setters (setStatusCode, setHeader, setContent, addCookie, json, redirect), Response provides immutable variants that return a new instance: withStatusCode(int), withHeader(name, value), withContent(string), withAddedCookie(Cookie).
- When writing middleware that decorates the response (e.g., adding security headers), prefer the immutable methods so upstream/downstream middleware don’t observe unexpected mutations.
- Open redirect protection: Response::redirect() rejects external URLs by default; pass allowExternal: true only for known-safe flows.
Compatibility note (PHP 8.5): There is a core bug where return clone($this)->method() may mutate $this instead of the clone. Arc’s with* implementations use a two-statement pattern ($new = clone $this; $new->method(); return $new;) to avoid this. If you implement your own immutable-style methods, avoid one-line clone-chains on PHP 8.5.
## Query Builder
`Arc\Database\QueryBuilder` provides a fluent interface for building SQL queries. All identifiers are validated against SQL injection.
```php
use Arc\Database\QueryBuilder;
$builder = new QueryBuilder($connection, 'users');
// SELECT with WHERE, ORDER BY, LIMIT
$users = $builder->where('active', 1)
->orderBy('name')
->limit(10)
->get();
// Operators: =, !=, <>, <, >, <=, >=, LIKE, NOT LIKE
$builders = $builder->where('price', '>', 100)->get();
// NULL checks
$builder->whereNull('deleted_at');
$builder->whereNotNull('email');
// IN / NOT IN
$builder->whereIn('role', ['admin', 'editor']);
$builder->whereNotIn('status', ['banned', 'suspended']);
// First row only (returns null if not found)
$user = $builder->where('id', 1)->first();
// Aggregates
$builder->count();
$builder->exists();
$builder->sum('price');
$builder->avg('price');
$builder->min('price');
$builder->max('price');
// INSERT
$id = $builder->insert(['name' => 'Arc', 'email' => 'arc@example.com']);
// UPDATE with WHERE
$builder->where('id', 1)->update(['name' => 'Updated']);
// DELETE with WHERE
$builder->where('id', 1)->delete();
// Select specific columns
$builder->select(['name', 'email'])->get();
```
## Database and Models
Configure the database connection in `config/database.php`, then extend the Model:
```php
use Arc\Database\Model;
class User extends Model
{
protected string $table = 'users';
protected string $primaryKey = 'id';
protected array $fillable = ['name', 'email'];
}
```
Available methods:
```php
User::all(limit: 50, offset: 0); // paginated retrieval
User::find($id); // single record or null
User::findOrFail($id); // single record or throws
User::where('email', 'user@example.com'); // conditional query
User::where('age', '>', 18); // with operator
User::create(['name' => 'Arc', 'email' => '...']);
User::update($id, ['name' => 'Updated']);
User::delete($id);
User::count(); // total count
User::exists(); // any rows exist
User::sum('age'); // aggregate
User::avg('age');
User::min('age');
User::max('age');
```
Fluent queries via `query()`:
```php
User::query()->where('active', 1)->orderBy('name')->limit(10)->get();
User::query()->whereNull('email')->count();
User::query()->whereIn('role', ['admin', 'editor'])->get();
```
Column names in `where()`, `create()`, and `update()` are validated against a strict regex (`/^[a-zA-Z_][a-zA-Z0-9_]*$/`) to prevent SQL injection. Invalid identifiers throw `InvalidArgumentException`.
## Validation
```php
use Arc\Validation\Validator;
$validator = Validator::make($_POST, [
'name' => 'required|string|min:2',
'email' => 'required|email',
'age' => 'integer|min:18',
]);
if ($validator->fails()) {
$errors = $validator->errors();
}
$validated = $validator->validated();
```
Available rules: `required`, `string`, `integer`, `numeric`, `email`, `url`, `boolean`, `min`, `max`, `between`, `same`, `different`, `in`, `not_in`, `alpha`, `alpha_num`, `regex`, `date`.
The `regex` rule uses `~` as delimiter (supports patterns containing `/`):
```php
'code' => 'regex:^[a-z]+\d+$'
```
Custom error messages:
```php
Validator::make($data, $rules, [
'email.required' => 'Please enter your email',
]);
```
## Session
```php
use Arc\Http\Session;
$session = new Session();
$session->start();
$session->set('user_id', 42);
$session->get('user_id'); // 42
$session->has('user_id'); // true
// Flash messages (persist for one request)
$session->setFlash('status', 'Saved successfully!');
$session->flash('status'); // 'Saved successfully!' (then cleared)
$session->regenerate(); // prevent session fixation
$session->destroy(); // end session
```
## File Uploads
```php
// Simple access
$file = $request->getFile('avatar');
// Validated access (checks size, MIME type, upload validity)
$file = $request->validateFile('avatar', maxBytes: 5_242_880, allowedMimes: ['image/jpeg', 'image/png']);
// Throws InvalidArgumentException on failure
```
## Configuration
Config files live in `config/` and return arrays:
```php
// config/app.php
return [
'name' => $_ENV['APP_NAME'] ?? 'Arc',
'env' => $_ENV['APP_ENV'] ?? 'production',
'debug' => (bool) ($_ENV['APP_DEBUG'] ?? false),
'url' => $_ENV['APP_URL'] ?? 'http://localhost',
'timezone' => 'UTC',
];
```
Access via the application:
```php
$app->config()->get('app.name'); // 'Arc'
$app->config()->get('app.debug'); // false
$app->config()->set('app.theme', 'dark');
```
## DI Container
The container supports explicit bindings, singletons, and auto-wiring:
```php
// Explicit binding
$app->bind(PaymentGateway::class, StripeGateway::class);
// Singleton (resolved once)
$app->singleton(Logger::class, FileLogger::class);
// Auto-wiring (resolves constructor dependencies)
$controller = $app->make(UserController::class);
```
Constructor parameters with class types are resolved from the container. Scalar parameters require defaults or explicit bindings.
## Error Handling
In production (`APP_DEBUG=false`), errors are logged and a generic error page is shown. In debug mode, full stack traces are displayed.
Database errors are wrapped in `DatabaseException` to prevent sensitive SQL and table names from leaking.
## CORS
`CorsMiddleware` handles cross-origin requests and preflight:
```php
use Arc\Http\Middleware\CorsMiddleware;
// Allow specific origins
$app->addMiddleware(new CorsMiddleware(allowedOrigins: ['https://app.example.com']));
// Allow all origins (for APIs)
$app->addMiddleware(new CorsMiddleware(allowedOrigins: '*'));
// With credentials and custom headers
$app->addMiddleware(new CorsMiddleware(
allowedOrigins: ['https://app.example.com'],
allowCredentials: true,
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-TOKEN'],
));
```
## HTTP Method Override
Browser forms only support GET and POST. Arc supports method spoofing via a hidden `_method` field or the `X-HTTP-Method-Override` header:
```html
```
Or via API header:
```
X-HTTP-Method-Override: PATCH
```
Only POST requests can be overridden to PUT, PATCH, or DELETE. Use `getOriginalMethod()` to see the actual HTTP method.
## Console Commands
```bash
arc new my-project # Create a new Arc project (scaffold, composer install)
arc serve # Start development server (port 8080)
arc serve --port 3000 # Custom port
arc serve --detach # Run in background
arc serve:stop # Stop the background server
arc route:list # List registered routes
arc make:controller UserController
arc make:model User
```
## License
MIT, see [LICENSE](LICENSE).
## Contributing
PRs welcome. Please open an issue first for major changes. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
## Security
See [SECURITY.md](SECURITY.md) for how to report vulnerabilities.