Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,60 @@ class UserService

Available helpers are `query($sql, $parameters)`, `execute($sql, $parameters)`, `pdo()`, and `transaction($callback)`. Query results expose `all()`, `first()`, `value()`, `affectedRows()`, and the raw `PDOStatement`.

### Query Builder and Models

Use the table query builder for simple fluent queries:

```php
$users = $db->table('users')
->select('id', 'email')
->where('active', true)
->orderBy('id', 'desc')
->limit(10)
->get();
```

Models extend `Shift\Database\Model`. Public properties represent database columns:

```php
use Shift\Database\Attributes\Cast;
use Shift\Database\Attributes\Guarded;
use Shift\Database\Attributes\PrimaryKey;
use Shift\Database\Model;

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

#[PrimaryKey]
#[Cast('int')]
public ?int $id = null;

public string $email = '';

#[Guarded]
public string $role = 'user';

#[Cast('array')]
public array $meta = [];

#[Cast('datetime')]
public ?DateTimeImmutable $created_at = null;
}
```

Model queries return hydrated model instances:

```php
$user = User::query($db)->where('email', 'dev@example.com')->first();
$user = User::find(1, $db);
$user = User::create(['email' => 'dev@example.com'], $db);
$user->role = 'admin';
$user->save($db);
```

`#[Guarded]` fields are ignored during mass assignment through `create()` and query `update()`, but can be set explicitly on a model instance before `save()`. Supported casts include `int`, `float`, `bool`, `string`, `array`, `date`, `datetime`, and class names. Class casts use `fromArray()` when available.

## Service Container

The container can resolve registered services and build classes with typed constructor dependencies:
Expand Down
1 change: 1 addition & 0 deletions REFACTORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ ShiftPHP is moving toward an API-only modular monolith. View templates, compiled
- [x] CLI create generators for modules and module-owned classes with file-based stubs.
- [x] `.env` loading for application configuration.
- [x] Native PDO database configuration, lazy connection, and basic query API.
- [x] Fluent query builder and attribute-driven database models.
- [x] Removal of view storage and example page assets from runtime.
- [x] Removal of legacy `application/controllers` and `application/routes.php`.
- [x] Domain-oriented framework namespaces:
Expand Down
44 changes: 44 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,50 @@ <h2>Environment and Database</h2>
}</code></pre>

<p>Use <code>query()</code> for prepared queries, <code>execute()</code> for write statements, <code>pdo()</code> for raw PDO access, and <code>transaction()</code> for transactional work.</p>

<h3>Query Builder</h3>
<pre><code>$users = $db-&gt;table('users')
-&gt;select('id', 'email')
-&gt;where('active', true)
-&gt;orderBy('id', 'desc')
-&gt;limit(10)
-&gt;get();</code></pre>

<h3>Models</h3>
<p>Models extend <code>Shift\Database\Model</code>. Public properties are database columns, and model attributes describe primary keys, guarded fields, and casts.</p>

<pre><code>use Shift\Database\Attributes\Cast;
use Shift\Database\Attributes\Guarded;
use Shift\Database\Attributes\PrimaryKey;
use Shift\Database\Model;

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

#[PrimaryKey]
#[Cast('int')]
public ?int $id = null;

public string $email = '';

#[Guarded]
public string $role = 'user';

#[Cast('array')]
public array $meta = [];

#[Cast('datetime')]
public ?DateTimeImmutable $created_at = null;
}</code></pre>

<pre><code>$user = User::query($db)-&gt;where('email', 'dev@example.com')-&gt;first();
$user = User::find(1, $db);
$user = User::create(['email' =&gt; 'dev@example.com'], $db);
$user-&gt;role = 'admin';
$user-&gt;save($db);</code></pre>

<p><code>#[Guarded]</code> fields are ignored during mass assignment through <code>create()</code> and query <code>update()</code>, but can be set explicitly before <code>save()</code>. Supported casts include <code>int</code>, <code>float</code>, <code>bool</code>, <code>string</code>, <code>array</code>, <code>date</code>, <code>datetime</code>, and class names. Class casts use <code>fromArray()</code> when available.</p>
</section>

<section id="cli">
Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/CreateModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function execute(mixed ...$args): void
$this->writeAndReport($path, $this->renderStub('model', [
'module' => $module,
'class' => $class,
'table' => NameFormatter::tableName($class),
]));
}

Expand Down
7 changes: 7 additions & 0 deletions src/Console/Generator/NameFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ public static function commandName(string $className): string
return implode(':', $parts);
}

public static function tableName(string $className): string
{
$snake = strtolower((string) preg_replace('/(?<!^)[A-Z]/', '_$0', $className));

return str_ends_with($snake, 's') ? $snake : $snake . 's';
}

public static function slug(string $name): string
{
$parts = preg_split('/[^a-zA-Z0-9]+/', $name, -1, PREG_SPLIT_NO_EMPTY) ?: [];
Expand Down
5 changes: 4 additions & 1 deletion src/Console/Generator/stubs/model.stub
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Modules\{{ module }}\Models;

class {{ class }}
use Shift\Database\Model;

class {{ class }} extends Model
{
protected string $table = '{{ table }}';
}
15 changes: 15 additions & 0 deletions src/Database/Attributes/Cast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Shift\Database\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
final class Cast
{
public function __construct(
public readonly string $type,
public readonly ?string $format = null
) {
}
}
10 changes: 10 additions & 0 deletions src/Database/Attributes/Guarded.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Shift\Database\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
final class Guarded
{
}
10 changes: 10 additions & 0 deletions src/Database/Attributes/PrimaryKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Shift\Database\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
final class PrimaryKey
{
}
10 changes: 10 additions & 0 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public function query(string $sql, array $parameters = []): QueryResult
}
}

public function table(string $table): QueryBuilder
{
return new QueryBuilder($this, $table);
}

public function execute(string $sql, array $parameters = []): int
{
return $this->query($sql, $parameters)->affectedRows();
Expand All @@ -71,6 +76,11 @@ public function transaction(callable $callback): mixed
}
}

public function lastInsertId(?string $name = null): string
{
return $this->pdo()->lastInsertId($name);
}

private function options(): array
{
return $this->config->options + [
Expand Down
Loading
Loading