PHPackages                             peixinho/lazymephp - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. peixinho/lazymephp

ActiveProject[Framework](/categories/framework)

peixinho/lazymephp
==================

LazyMePHP is a PHP framework designed for rapid development with a focus on class, form, and REST API generation from database tables.

v1.0.0(1mo ago)544MITPHPPHP ^8.0CI failing

Since Jul 15Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/Peixinho/LazyMePHP)[ Packagist](https://packagist.org/packages/peixinho/lazymephp)[ Docs](https://github.com/Peixinho/LazyMePHP)[ RSS](/packages/peixinho-lazymephp/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (7)Versions (3)Used By (0)

  ![LazyMePHP](https://raw.githubusercontent.com/Peixinho/LazyMePHP/main/public/img/logo.png)LazyMePHP is a PHP 8+ rapid-development framework built around a single idea: **the database schema is the application**. Point it at a database and you get a full CRUD web UI, a GraphQL API, JWT-authenticated REST endpoints, and a developer dashboard — with zero code generation.

- MySQL, SQLite, and MSSQL support
- Runtime ORM — no generated model files
- Generic CRUD web UI driven by the live schema
- GraphQL API auto-built from the schema (`POST /graphql`)
- JWT authentication with refresh tokens for SPA / API consumers
- Role-based access control (RBAC)
- Database migration system
- Seeder and factory system for test data
- Audit log for all data mutations
- Batman developer dashboard with secure login
- Schema file cache for OPcache-friendly production deployments
- OpenAPI 3.0 spec auto-generated from live schema (`GET /openapi.json`)
- Health check endpoint (`GET /health`)
- Request ID tracing on every response (`X-Request-ID`)
- Pluggable cache layer: Redis, APCu, or in-process array
- General-purpose rate limiting middleware
- Background queue system: sync, database, or Redis drivers
- Standalone `FormRequest` validation (controller-level, no model required)
- File storage abstraction with local disk driver
- Multi-tenancy support (subdomain, header, path, or JWT resolution)

> Only `public/` should be web-accessible.

---

Quick start
-----------

[](#quick-start)

```
git clone https://github.com/Peixinho/LazyMePHP myProject
cd myProject && rm -rf .git
composer install
cp .env.example .env   # edit DB_* and APP_* values
php LazyMePHP migrate  # create framework tables
php LazyMePHP serve
```

Navigate to `http://localhost:8080`. Every table in the database is immediately accessible at `/{table}` with list, create, edit, and delete pages, and via the GraphQL endpoint at `POST /graphql`.

---

Configuration
-------------

[](#configuration)

All settings live in `.env`:

VariableDescription`DB_TYPE``mysql`, `mssql`, or `sqlite``DB_HOST`Database host (MySQL / MSSQL)`DB_NAME`Database name (MySQL / MSSQL)`DB_USER`Database username (MySQL / MSSQL)`DB_PASSWORD`Database password (MySQL / MSSQL)`DB_FILE_PATH`Path to SQLite file`APP_NAME`Application name`APP_TITLE`HTML page title`APP_TIMEZONE`PHP timezone string (e.g. `Europe/Lisbon`)`APP_NRESULTS`Default page size for paginated lists`APP_ENCRYPTION`Secret key (≥ 32 chars) — used for JWT signing`APP_ENV``development` enables GraphQL introspection and debug traces`APP_CORS_ORIGIN`Exact origin allowed for cross-origin requests (empty = block all)`APP_ACTIVITY_LOG``true` to enable change audit logging`APP_ACTIVITY_AUTH`Fallback identifier written to the audit log when no JWT user is present`AUTH_TABLE`Table used for JWT login (enables `POST /auth/login`)`AUTH_USERNAME_COLUMN`Column checked as the login credential`AUTH_PASSWORD_COLUMN`Column holding the bcrypt-hashed password`AUTH_TOKEN_TTL`JWT lifetime in seconds (default `3600`)`AUTH_REFRESH_TTL`Refresh token lifetime in seconds (default `2592000` = 30 days)`BATMAN_USERNAME`Batman dashboard login username (default `admin`)`BATMAN_SECRET`Batman dashboard password as a bcrypt hash`OPENAPI_ENABLED`Set to `false` to disable the `/openapi.json` endpoint`CACHE_DRIVER`Cache backend: `array` (default), `apcu`, `redis``REDIS_HOST`Redis host (default `127.0.0.1`)`REDIS_PORT`Redis port (default `6379`)`REDIS_PASSWORD`Redis password (empty = no auth)`REDIS_DB`Redis database index (default `0`)`QUEUE_DRIVER`Queue backend: `sync` (default), `database`, `redis``STORAGE_DRIVER`Storage driver: `local` (default)`STORAGE_PATH`Root directory for local storage (default `storage/app`)`STORAGE_URL`Public URL prefix for stored files (default `/storage`)`TENANT_TABLE`Tenants table name (enables multi-tenancy when set)`TENANT_COLUMN`Column used to look up the tenant (default `slug`)`TENANT_RESOLVE`How to identify the tenant: `subdomain` | `header` | `path` | `jwt``TENANT_REQUIRE`Return 400/404 if no tenant found (default `true`)---

How it works
------------

[](#how-it-works)

On every request `LazyMePHP::boot($blade)` (called from `App/Routes/Routes.php`):

1. Reads the list of tables from the schema cache, or queries the DB directly.
2. Emits a `X-Request-ID` header for tracing.
3. Registers 6 CRUD web routes per table via `Core\AutoRouter`.
4. Registers `POST /graphql` via `Core\GraphQL\Endpoint`.
5. Registers `POST /auth/login`, `POST /auth/logout`, `POST /auth/refresh`, `GET /auth/me` when `AUTH_TABLE` is set.
6. Registers `GET /health` (health check) and `GET /openapi.json` (OpenAPI spec).

No files are generated. Schema introspection happens once per table per process (cached in memory), and optionally pre-warmed to disk for production.

---

ORM — `Core\Model`
------------------

[](#orm--coremodel)

`Model` introspects the DB schema at runtime and provides full CRUD.

```
use Core\Model;

// Create
$user = new Model('users');
$user->name  = 'Alice';
$user->email = 'alice@example.com';
$user->Save();

// Load by primary key
$user = new Model('users', 1);
echo $user->name; // Alice

// Update
$user->name = 'Alice Smith';
$user->Save();

// Delete
$user->Delete();
```

### Query builder

[](#query-builder)

```
$active = Model::query('users')
    ->where('active', 1)
    ->where('age', 18, '>=')
    ->orderBy('name')
    ->limit(20)
    ->get();  // returns Model[]

$count = Model::query('users')->where('active', 1)->count();

$row = Model::query('users')->where('email', $email)->first();
```

Other `where` variants:

```
->whereLike('name', '%alice%')
->whereNull('deleted_at')
->whereNotNull('verified_at')
->whereIn('status', ['active', 'trial'])
->whereRaw('"score" > ? OR "admin" = 1', [50])   // raw SQL, AND by default
->whereRaw('"role" = ?', ['editor'], 'OR')         // change connector
```

### Joins

[](#joins)

```
$rows = Model::query('orders')
    ->join('customers', 'orders.customer_id', 'customers.id')
    ->leftJoin('coupons', 'orders.coupon_id', 'coupons.id')
    ->select('orders.*', 'customers.name AS customer_name', 'coupons.code AS coupon')
    ->where('orders.status', 'open')
    ->orderBy('orders.created_at', 'DESC')
    ->get();

// Columns from joined tables and aliases come through as model properties:
echo $rows[0]->customer_name;
echo $rows[0]->coupon;       // null when left-join partner is missing
```

Available join methods: `join()` (INNER), `leftJoin()`, `rightJoin()`.

### Column selection and aggregates

[](#column-selection-and-aggregates)

```
// Restrict columns
Model::query('users')->select('id', 'name', 'email')->get();

// Aggregate expressions
$rows = Model::query('orders')
    ->select('customer_id', 'SUM(total) AS revenue', 'COUNT(*) AS cnt')
    ->groupBy('customer_id')
    ->having('revenue', 1000, '>=')
    ->orderBy('revenue', 'DESC')
    ->get();

echo $rows[0]->revenue;
echo $rows[0]->cnt;
```

`having(column, value, operator)` defaults to `=`. Runs after `GROUP BY`.

### Raw queries and `Model::hydrate()`

[](#raw-queries-and-modelhydrate)

For SQL that `ModelQuery` cannot express — CTEs, `UNION`, window functions, subqueries in `FROM`:

```
$result = LazyMePHP::DB_CONNECTION()->query('
    WITH ranked AS (
        SELECT *, RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnk
        FROM "employees"
    )
    SELECT * FROM ranked WHERE rnk = 1
', []);

$rows = [];
while ($row = $result->fetchArray()) $rows[] = $row;

$models = Model::hydrate('employees', $rows);
// schema columns + computed aliases (rnk) all accessible as properties
echo $models[0]->name;
echo $models[0]->rnk;
```

### Pagination

[](#pagination)

```
$result = Model::query('users')
    ->where('active', 1)
    ->paginate(perPage: 15, page: 2);

// $result = [
//   'data'         => Model[],
//   'total'        => 120,
//   'per_page'     => 15,
//   'current_page' => 2,
//   'last_page'    => 8,
//   'from'         => 16,
//   'to'           => 30,
// ]
```

### Bulk operations

[](#bulk-operations)

```
// Bulk update every row matching the query
Model::query('users')
    ->where('trial', 1)
    ->update(['active' => 0, 'trial' => 0]);

// Bulk delete matching rows
Model::query('users')->where('deleted_at', null, '!=')->bulkDelete();

// Bulk insert (returns number of rows inserted)
Model::insertMany('tags', [
    ['name' => 'php'],
    ['name' => 'framework'],
]);
```

### Transactions

[](#transactions)

```
use Core\Model;

Model::transaction(function () {
    $order = new Model('orders');
    $order->user_id = 1;
    $order->Save();

    $item = new Model('order_items');
    $item->order_id = $order->getPrimaryKey();
    $item->product_id = 42;
    $item->Save();
});
// Automatically rolled back on exception.
```

### Subclassing (optional)

[](#subclassing-optional)

```
namespace Models;
use Core\Model;

class User extends Model {
    protected static string $table = 'users';
}

$user  = new User(1);
$users = User::query()->where('active', 1)->get();
```

---

Model relationships
-------------------

[](#model-relationships)

```
class Post extends Model {
    protected static string $table = 'posts';

    public function author(): ?Model {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function comments(): array {
        return $this->hasMany(Comment::class, 'post_id');
    }
}

// Eager loading (prevents N+1)
$posts = Post::query()->with('author', 'comments')->get();
```

Supported: `belongsTo`, `hasMany`, `hasOne`, `belongsToMany`.

---

Soft deletes
------------

[](#soft-deletes)

Add `deleted_at DATETIME NULL` to a table, then use the trait:

```
use Core\Model;
use Core\SoftDeletes;

class Post extends Model {
    use SoftDeletes;
    protected static string $table = 'posts';
}

$post->Delete();          // sets deleted_at, row stays in DB
$post->restore();         // clears deleted_at
$post->isTrashed();       // true if deleted_at is set

// Queries automatically exclude soft-deleted rows:
Post::query()->get();                    // only non-deleted
Post::query()->withTrashed()->get();     // include deleted
Post::query()->onlyTrashed()->get();     // only deleted
```

---

Model validation
----------------

[](#model-validation)

```
class User extends Model {
    protected static string $table = 'users';

    protected static array $rules = [
        'name'  => 'required|min:2|max:100',
        'email' => 'required|email',
        'age'   => 'integer|min:0',
        'role'  => 'in:admin,editor,viewer',
        'site'  => 'url',
    ];
}

$user->name  = '';
$user->email = 'not-an-email';

if (!$user->passes()) {
    print_r($user->errors());
    // ['name' => ['The name field is required.'], 'email' => ['...must be a valid email']]
}

// Or get all errors at once:
$errors = $user->validate();
```

Available rules: `required`, `email`, `integer`, `numeric`, `min:N`, `max:N`, `in:a,b,c`, `url`, `boolean`.

---

Model events
------------

[](#model-events)

```
use Core\Events\ModelEvents;

// Listen for any save on 'orders'
ModelEvents::listen('orders', 'created', function (Model $order) {
    // send confirmation email
});

// Cancel a delete by returning false
ModelEvents::listen('orders', 'deleting', function (Model $order) {
    if ($order->status === 'completed') return false;
});

// Observer class
class OrderObserver {
    public function creating(Model $m): void { /* set defaults */ }
    public function updated(Model $m): void  { /* clear cache */ }
}

ModelEvents::registerObserver('orders', new OrderObserver());
// or on the model class:
Order::observe('orders', new OrderObserver());
```

Events fired: `creating`, `created`, `updating`, `updated`, `deleting`, `deleted`, `saving`, `saved`.
Returning `false` from `creating`, `updating`, or `deleting` cancels the operation.

---

Global scopes
-------------

[](#global-scopes)

Apply automatic query constraints to every query on a model:

```
class ActiveUser extends Model {
    protected static string $table = 'users';
    protected static array $globalScopes = [];
}

// Register once (e.g. in a service provider or boot):
ActiveUser::addGlobalScope('active', fn($q) => $q->where('active', 1));

ActiveUser::query()->get();                   // WHERE active = 1 always applied
ActiveUser::withoutGlobalScopes()->get();     // bypass all scopes
ActiveUser::removeGlobalScope('active');      // remove permanently
```

---

Local scopes
------------

[](#local-scopes)

Define reusable query constraints on the model class:

```
class Product extends Model {
    protected static string $table = 'products';

    public function scopeActive(\Core\ModelQuery $q): void {
        $q->where('active', 1);
    }

    public function scopePricedBelow(\Core\ModelQuery $q, float $max): void {
        $q->where('price', $max, '
