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
44 changes: 23 additions & 21 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ apply pending schema migrations before doing their work.
but the command opened a raw PDO without running pending
migrations. The rebuild then fired against the pre-2.2.0 schema
(`fields.searchable = 0` everywhere) and silently produced an
empty/incorrect FTS body for every item. Hit while bumping
Scriptor; fixed before reaching the Hetzner demo. (#60)
empty/incorrect FTS body for every item. Caught during a
downstream lock-bump, before the broken recipe reached
production. (#60)
- **`optimize` now auto-migrates too.** Same surprise was latent:
running `VACUUM` before migrations wastes cycles because the next
command applies new tables/columns anyway. (#60)
Expand All @@ -34,7 +35,7 @@ apply pending schema migrations before doing their work.
now applies migration `0005` first, then rebuilds against the
correct schema.
- Diagnostic commands (`dump`, `repair`, `schema:status`) are
unchanged — they're expected to report on the actual on-disk
unchanged. They're expected to report on the actual on-disk
state, so they must NOT auto-migrate.

## [2.2.0] — 2026-05-17
Expand Down Expand Up @@ -67,18 +68,19 @@ Design spec: [`docs/imanager-2.2-plan.md`](docs/imanager-2.2-plan.md).
- **Migration `0005_searchable_defaults.sql`.** Promotes existing
`text`/`longtext`/`editor`/`slug` field rows to `searchable = 1`
on upgrade so installs keep their existing FTS coverage for
prose content. Verified against the live Scriptor schema: all 8
text-typed fields (slug, parent, pagetype, menu_title, content,
template, role, email) promote; password + fileupload fields
stay at 0. (#58)
- **`FtsBody::compose()`** — single source of truth for the body
prose content. Verified against a representative production
schema: all text-typed field rows promote; `password` and
file-upload field rows stay at 0. (#58)
- **`FtsBody::compose()`**: single source of truth for the body
column written into `items_fts`. Used by both the per-save writer
and the bulk rebuilder so they cannot drift. (#58)
- **`SqliteItemRepository` constructor accepts optional
`?FieldRepository`** as the third argument. The 2-arg signature
from 2.0/2.1 keeps working falls back to "index everything"
from 2.0/2.1 keeps working: it falls back to "index everything"
with a one-time `E_USER_DEPRECATED` notice on first FTS write.
The no-arg form will become an error in 3.0. (#58)
- **`docs/imanager-2.2-plan.md`**: design plan with the
recommended-lean decision log and open-questions register. (#56)

### Upgrading from 2.1.x

Expand Down Expand Up @@ -135,36 +137,36 @@ keeps working. Design spec: [`docs/imanager-2.1-plan.md`](docs/imanager-2.1-plan

### Added

- **16 static factories on `Field`** for declarative schema setup
- **16 static factories on `Field`** for declarative schema setup:
`Field::text()`, `longText()`, `editor()`, `slug()`, `password()`,
`integer()`, `decimal()`, `money()`, `checkbox()`, `dropdown()`,
`datepicker()`, `hidden()`, `arrayList()`, `file()`, `image()`,
`filePicker()`. Each returns a fresh (`id = null`) `Field` with the
corresponding `FieldType` set and default flags. (#47)
- **Fluent setters on `Field` — general (6)**: `required(bool=true)`,
- **General fluent setters on `Field` (6)**: `required(bool=true)`,
`indexed(bool=true)`, `searchable(bool=true)`, `position(int)`,
`label(string)`, `config(array)`. Each returns a new
`final readonly` clone, preserving the value-object semantics. (#47)
- **Fluent setters on `Field` — type-aware (7)**: `maxLength(int)`,
- **Type-aware fluent setters on `Field` (7)**: `maxLength(int)`,
`minLength(int)`, `placeholder(string)`, `maxBytes(int)`,
`mimes(string ...)`, `options(array)`, `format(string)`. Each writes
one documented key into `config`; unrecognised keys are silently
ignored by built-in plugins, so a setter that doesn't apply to a
given `FieldType` is a no-op. (#47)
- **`CategoryRepository::ensure(Category): Category`** — upsert by
- **`CategoryRepository::ensure(Category): Category`**. Upsert by
natural key (`slug`). Insert-on-miss, return-existing-on-hit.
Emits `CategoryCreated` only on insert. (#48)
- **`FieldRepository::ensure(Field): Field`** — upsert by natural key
- **`FieldRepository::ensure(Field): Field`**. Upsert by natural key
`(categoryId, name)`. Same semantics as `CategoryRepository::ensure()`.
Emits `FieldCreated` only on insert. (#48)
- **`docs/imanager-2.1-plan.md`**design plan with the full surface
+ naming rationale + open-questions log. (#46)
- **`docs/imanager-2.1-plan.md`**: design plan with the full surface,
naming rationale, and open-questions log. (#46)

### Fixed

- **`Imanager::VERSION` is now bumped in lockstep with the git tag.**
Previously the constant was set to `2.0.0` at 2.0 release and never
moved at 2.0.1 or 2.0.2 `vendor/bin/imanager --version` reported
moved at 2.0.1 or 2.0.2, so `vendor/bin/imanager --version` reported
the wrong value against any newer install. New `ReleaseConsistencyTest`
asserts the constant matches the top-most `[X.Y.Z]` entry in
CHANGELOG.md so this can't silently rot again.
Expand All @@ -174,8 +176,8 @@ keeps working. Design spec: [`docs/imanager-2.1-plan.md`](docs/imanager-2.1-plan
`ensure()` is a new interface method on `CategoryRepository` and
`FieldRepository`. Direct callers of the existing methods need no
changes. **Third-party implementers** of these interfaces (no known
implementers in the wild as of this release) need to add `ensure()`
— the canonical 4-line implementation is documented in the JSDoc
implementers in the wild as of this release) need to add `ensure()`.
The canonical 4-line implementation is documented in the JSDoc
of each interface method.

## [2.0.2] — 2026-05-16
Expand All @@ -188,7 +190,7 @@ of each interface method.
copy-paste of the README quickstart no longer trips on PDO's
`unable to open database file` error. Hosts that hand-wire via
`Imanager\Bootstrap::boot()` keep full control of directory
lifecyclethe convenience is specific to the copy-paste factory.
lifecycle; the auto-`mkdir` is specific to the copy-paste factory.
(#40)

### Fixed
Expand All @@ -197,7 +199,7 @@ of each interface method.
parent directory** and suggests `mkdir -p <dir>` (or bootstrapping
via `DefaultBootstrap`, which now does that itself) when the
SQLite open fails because the parent doesn't exist. The previous
error surfaced as the raw PDO "unable to open database file"
error surfaced as the raw PDO "unable to open database file",
opaque even to seasoned PHP devs. (#40)

### Changed
Expand Down
51 changes: 23 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ iManager is a small CMS **framework**, not a CMS application: you embed
it inside your own PHP app and get a typed domain model, a Repository
layer over SQLite (JSON columns + FTS5), a Field-Type plugin system,
file storage with on-demand thumbnails, and a CLI for schema and
migration ops. Use it under any PHP front-end you like a hand-rolled
migration ops. Use it under any PHP front-end you like: a hand-rolled
admin tool, a flat-file CMS, an internal API, a static-site generator
that needs a typed content store. iManager has no opinion about how
your application is shaped.
Expand All @@ -17,7 +17,7 @@ your application is shaped.

## Status

**Stable** — current line **2.0.x** (latest: 2.0.2, 2026-05-16).
**Stable**. Current line **2.2.x** (latest: 2.2.1, 2026-05-17).

The 1.x line (flat-file, `var_export`-based, embedded library shape)
stays available for legacy installs; see the
Expand Down Expand Up @@ -59,20 +59,18 @@ $categories = $container->get(CategoryRepository::class);
$fields = $container->get(FieldRepository::class);
$items = $container->get(ItemRepository::class);

// One-time schema setup — run from an installer / migration, not on every
// request. Repeating these three calls against the same database raises a
// UNIQUE-constraint error; guard them with findBySlug() / findByName() if
// you want them to be idempotent.
$blog = $categories->save(new Category(null, 'Blog', 'blog'));
$fields->save(new Field(null, $blog->id, 'title', 'Title', FieldType::Text));
$fields->save(new Field(null, $blog->id, 'body', 'Body', FieldType::LongText));
// Schema setup: ensure() is idempotent (insert-on-miss, return-on-hit),
// so this block is safe to run on every boot.
$blog = $categories->ensure(new Category(null, 'Blog', 'blog'));
$fields->ensure(new Field(null, $blog->id, 'title', 'Title', FieldType::Text));
$fields->ensure(new Field(null, $blog->id, 'body', 'Body', FieldType::LongText));

// Persist an item.
$items->save(new Item(
null,
$blog->id,
'hello-world', // name URL-friendly identifier
'Hello, world', // label human-readable title
'hello-world', // name: URL-friendly identifier
'Hello, world', // label: human-readable title
data: ['title' => 'Hello, world', 'body' => 'First post.'],
));

Expand All @@ -91,7 +89,7 @@ pick up new migrations the same way.

Need a leaner container or want to swap PDO / FileStorage / the
event dispatcher? Use `Imanager\Bootstrap::boot()` instead and wire
the parts you want `DefaultBootstrap` is just a copy-paste-saver
the parts you want. `DefaultBootstrap` is just a copy-paste-saver
on top of it.

---
Expand All @@ -100,17 +98,17 @@ on top of it.

iManager models content as four primitives:

- **Category** a kind of thing (e.g. *Blog*, *Page*, *User*). Each
- **Category**: a kind of thing (e.g. *Blog*, *Page*, *User*). Each
category has its own field schema and its own slug.
- **Field** a typed column on a category. The built-in field types
- **Field**: a typed column on a category. The built-in field types
are: `text`, `longtext`, `editor`, `slug`, `password`, `integer`,
`decimal`, `money`, `checkbox`, `dropdown`, `datepicker`, `hidden`,
`array`, `fileupload`, `imageupload`, `filepicker`. Custom types
register via the `FieldTypePlugin` interface.
- **Item** an instance of a category. Field values live in a typed
- **Item**: an instance of a category. Field values live in a typed
`FieldValueBag` exposed as `$item->data`; hot fields are also
promoted to SQLite generated columns for indexable queries.
- **File** a binary asset (upload). Files are stored under
- **File**: a binary asset (upload). Files are stored under
`<uploadsPath>/<itemId>/<fieldId>/` (the `uploadsPath` you pass to
`DefaultBootstrap::boot()`), with on-demand thumbnails for image
uploads under `thumbnail/<W>x<H>_<file>`.
Expand All @@ -134,7 +132,7 @@ container (`docker compose run --rm imanager vendor/bin/imanager …`).
| `schema:migrate` | Apply pending migrations. |
| `migrate:from-v1`| One-shot import of a 1.x `data/datasets/buffers/` tree. Supports `--dry-run`. |
| `fts:rebuild` | Drop & rebuild the FTS5 index from `items`. |
| `optimize` | `PRAGMA optimize` + `VACUUM`. |
| `optimize` | `PRAGMA optimize`. Add `--vacuum` to also run `VACUUM`. |
| `repair` | Integrity checks (orphan items, broken FKs, FTS sync). |
| `dump` | Portable SQL dump. |

Expand Down Expand Up @@ -175,26 +173,23 @@ Available composer scripts:

## Docs

- **Tutorial**: [`docs/tutorial/`](docs/tutorial/) task-oriented
- **Tutorial**: [`docs/tutorial/`](docs/tutorial/), task-oriented
walkthroughs for newcomers (setup, schema design, validation, …).
Start here if you just installed the package and want a guided
path past the Quickstart.
- **API reference**: [`docs/api/`](docs/api/) index plus core
- **API reference**: [`docs/api/`](docs/api/), index plus core
detail pages for Domain, Storage, Query, and Field types.
- **Field-types cookbook**: [`docs/field-types.md`](docs/field-types.md)
how-to companion to the Field-types reference.
- **Query cookbook**: [`docs/query-cookbook.md`](docs/query-cookbook.md)
predicate recipes, pagination, selector strings, full-text-search
- **Field-types cookbook**: [`docs/field-types.md`](docs/field-types.md),
how-to companion to the Field-types reference.
- **Query cookbook**: [`docs/query-cookbook.md`](docs/query-cookbook.md),
predicate recipes, pagination, selector strings, full-text-search
hand-off, performance.
- **Deployment guide**: [`docs/deployment.md`](docs/deployment.md)
host requirements, webserver + PHP-FPM configs, a production
- **Deployment guide**: [`docs/deployment.md`](docs/deployment.md),
host requirements, webserver + PHP-FPM configs, a production
Dockerfile, SQLite at runtime, backups, scheduled maintenance.
- **Migration guide** (1.x → 2.0):
[`docs/migration-guide.md`](docs/migration-guide.md).
- **Changelog**: [`CHANGELOG.md`](CHANGELOG.md).
- **Implementation history** — the multi-phase 1.x → 2.0 rewrite plan,
kept for context now that the work is done:
[`docs/imanager-2.0-plan.md`](docs/imanager-2.0-plan.md).

---

Expand Down
26 changes: 13 additions & 13 deletions docs/api/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# API Reference

Reference documentation for the iManager 2.0 library — every public class
Reference documentation for the iManager 2 library. Every public class
you can wire, extend, or call from a host application.

> **Where to start:** the [README quickstart](../../README.md#quickstart)
Expand All @@ -14,22 +14,22 @@ you can wire, extend, or call from a host application.
## Core (covered in detail here)

The four pages below cover ~90 % of what host code touches. Read them
in order if you're new — each one builds on the previous.
in order if you're new. Each one builds on the previous.

- **[Domain](domain.md)** `Category`, `Field`, `Item`, `File`,
- **[Domain](domain.md)**: `Category`, `Field`, `Item`, `File`,
`FieldValueBag`, and the nine domain events (`*Created`, `*Updated`,
`*Deleted`). All domain objects are `final readonly`; you mutate by
saving a new value.
- **[Storage](storage.md)** the `Storage` interface and its four
- **[Storage](storage.md)**: the `Storage` interface and its four
repositories (`CategoryRepository`, `FieldRepository`,
`ItemRepository`, `FileRepository`), `SqliteStorage` (the only
bundled implementation), and the `transactional()` boundary. Also
covers `SchemaManager` and `Migration`.
- **[Query](query.md)** the immutable `Query` builder, the
- **[Query](query.md)**: the immutable `Query` builder, the
`Clause` / `Operator` / `OrderBy` / `Direction` value objects,
`Pagination`, and the `SelectorParser` shorthand
(`name=Hello*, position>=5`).
- **[Field types](field-types.md)** the `FieldTypePlugin`
- **[Field types](field-types.md)**: the `FieldTypePlugin`
interface, `FieldTypeRegistry`, the 16 built-in plugins, and the
`ValidationResult` / `RenderContext` value objects you return /
receive when writing your own type.
Expand Down Expand Up @@ -63,16 +63,16 @@ tells you which source file to read next.

## Conventions used in this reference

- **`final readonly`** every domain primitive and most value objects
- **`final readonly`**: every domain primitive and most value objects
are immutable. Operations like `withId()`, `withTitle()`, or the
builder methods on `Query` return a new instance.
- **Method signatures** are quoted **verbatim** from the source same
- **Method signatures** are quoted **verbatim** from the source: same
parameter names, defaults, and return types. If something looks
surprising, check the file path at the top of each page; the source
wins, this doc follows.
- **Examples** are lifted from the contract tests under
`tests/Unit/Storage/*Contract.php`. Those tests run against both
`SqliteStorage` and the `InMemory` storage shipped for testing so
`SqliteStorage` and the `InMemory` storage shipped for testing, so
every example you see compiles and is exercised on every CI run.
- **Exceptions thrown** are listed under each method that can throw.
Methods without a "Throws" line never throw in normal operation
Expand All @@ -85,22 +85,22 @@ tells you which source file to read next.
The four core pages cover the public API any application boots
against. Companion guides:

- [`docs/field-types.md`](../field-types.md) **cookbook** for
- [`docs/field-types.md`](../field-types.md), the **cookbook** for
writing custom field types end-to-end (anatomy, validation
patterns, rendering patterns, registration, testing). The
reference page [`field-types.md`](field-types.md) is the matching
index.
- [`docs/query-cookbook.md`](../query-cookbook.md) **cookbook**
- [`docs/query-cookbook.md`](../query-cookbook.md), the **cookbook**
for the `Query` builder: predicate recipes, pagination flows,
selector strings, full-text-search hand-off, performance.
Matching reference page: [`query.md`](query.md).
- [`docs/deployment.md`](../deployment.md) **deployment guide**:
- [`docs/deployment.md`](../deployment.md), the **deployment guide**:
host requirements, filesystem layout, Caddy / nginx + PHP-FPM
configs, a production Dockerfile, SQLite at runtime (WAL files,
backups), scheduled maintenance, logging hookup, production
checklist.

The smaller subsystems (Cache, Templating, Http, Events, Validation)
will get their own reference pages **only if** non-trivial host
extension is expected — for now their source files are short and
extension is expected. For now their source files are short and
documented inline.
Loading
Loading