Search by

kksonthomas / kkson-framework

kksonthomas

self use framework

Package info

github.com/kksonthomas/kkson-framework

pkg:composer/kksonthomas/kkson-framework

Statistics

Installs: 188

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.11.2.0 2026-09-17 11:09 UTC

README

PHP admin/CRUD framework built on Slim 2, RedBeanPHP, Plates, and AdminLTE. Includes authentication, CRUD UI, search/export, permissions, and system logging.

Installation

composer require kksonthomas/kkson-framework:^0.11

New blank project: copy the packaged scaffold, configure conf/, and import the greenfield SQL. Step-by-step: docs/getting-started.md.

# After composer require in an empty project directory:
# Windows: Copy-Item -Recurse vendor\kksonthomas\kkson-framework\scaffold\* .
# bash:    cp -a vendor/kksonthomas/kkson-framework/scaffold/. .
mysql -u USER -p DATABASE < vendor/kksonthomas/kkson-framework/sql/0000_init_framework.sql
Material Path
Getting started docs/getting-started.md
Blank app scaffold scaffold/
Greenfield DB init sql/0000_init_framework.sql

Add configuration under your application conf/ directory (scaffold ships .example files):

  • app.config.ini — environment and app settings (env selects the DB config file)
  • db.config.{env}.ini — database connection

Default seed login after init SQL: sysadmin / sysadmin (change after first login). New databases use the init file only; existing DBs missing system_log.client_ip still use sql/patch-v0.10.4.1-ip-ban.sql.

Database transactions and Writer Cache (v0.11.0.0+)

Updating to v0.11.0.0+ enables transactional CRUD by default and adds DB::begin(), DB::commit(), DB::rollback(), and DB::transaction() with Writer Cache flush on rollback.

composer require kksonthomas/kkson-framework:^0.11.0.0

CRUD insert, update, and delete run inside a DB transaction by default. Opt out on a CRUD instance:

$crud->setIsInsertUpdateUseTransaction(false);

Transactions require frozen RedBean (DB::fixSchema() / R::freeze(true) in normal app bootstrap).

DB transaction API

Use KKsonFramework\App\DB for transaction control instead of importing R for begin / commit / rollback:

use KKsonFramework\App\DB;

DB::begin();
try {
    // ...
    DB::commit();
} catch (\Throwable $e) {
    DB::rollback(); // rolls back + flushes RedBean Writer Cache
    throw $e;
}

// or
DB::transaction(function () {
    // ...
});

Why rollback flushes cache

RedBean 5.7 Writer Cache (default ON) caches R::find, R::findOne, R::load, and related read queries. R::store and R::exec invalidate the cache on the next cached read; R::rollback() does not. After a rollback, cached reads can still return rows from the undone transaction.

DB::rollback() and a failed DB::transaction() call R::getWriter()->flushCache() as RedBean recommends. Successful commits do not flush cache (committed data matches the cache). Direct SQL via R::getCell / R::getAll is not Writer-cached.

Custom code that still uses R::begin() / R::rollback() directly should switch to DB::* or call R::getWriter()->flushCache() after rollback.

IP ban performance (v0.10.4.1+)

Updating the package improves IP ban behavior without any database change. Unauthenticated requests only check existing bans; failed-login counting runs after a failed login.

For faster queries on large system_log tables on databases created before the greenfield init, apply the optional SQL patch once:

vendor/kksonthomas/kkson-framework/sql/patch-v0.10.4.1-ip-ban.sql

Example:

mysql -u USER -p DATABASE < vendor/kksonthomas/kkson-framework/sql/patch-v0.10.4.1-ip-ban.sql

The script adds system_log.client_ip, backfills simple JSON array rows, and creates indexes. Skip it if you do not need indexed IP lookups, or if the database was created with sql/0000_init_framework.sql (already includes client_ip and indexes). The app remains fully functional either way.

On re-run, ignore duplicate column or duplicate index errors. Requires system_log and ban_ip_list tables.

Soft delete (mimic delete)

Models can soft-delete by setting _deleted = 1 instead of removing rows. Enable on a BaseModelBase subclass:

public static function _enabledMimicDelete() {
    return true;
}

The table must have an integer _deleted column (0 = active, 1 = deleted).

Loading

  • Model::load($id) — active rows only (_deleted = 0).
  • Model::load($id, true) — includes soft-deleted rows.
  • Model::loadForUpdate($id) — same filter as load(); use loadForUpdate($id, true) to lock a deleted row.
  • Model::isActiveInDb() — re-reads _deleted from the database for long-running jobs.

Saving and concurrency

R::store() runs the FUSE update() hook on boxed models. For mimic-delete tables, BaseModelBase::update() rejects a stale revive: the database row is soft-deleted (_deleted = 1) while the in-memory bean still has _deleted = 0. This prevents cross-request/cron races from undoing a delete after deleteSelf() or CRUD delete.

  • Restore a row with undeleteSelf() or save(allowReviveDeleted: true).
  • Otherwise catch KKsonFramework\RedBeanPHP\Exception\StaleDeletedModelException and skip the write (typical for background jobs).

Subclasses that override update() must call parent::update() first so the guard runs.

CRUD transactions

CRUD insert, update, and delete are transactional by default (see Database transactions and Writer Cache (v0.11.0.0+)). This wraps one HTTP request only; it does not cover CLI, cron, or a second concurrent request. Long jobs should re-check isActiveInDb() before saving or handle StaleDeletedModelException.

Audit fields (v0.11.2.0+)

BaseModelBase assigns creation_date, creation_user_id, modified_date, and modified_user_id on every R::store() / save(). That is the default. High-volume fact tables that do not have these columns can opt out:

public static function _enabledAuditFields(): bool
{
    return false;
}

Default is true, so existing models keep current behaviour. This flag is independent of _enabledMimicDelete() / _deleted: a table can have audit and no soft-delete (the usual case), or neither.

When the flag is false, FUSE update() still runs mimic-delete checks and tempID bookkeeping, but it does not assign the four properties. Do not call parent::update() and then unset() the columns — that still writes them onto the bean first, which errors under a frozen schema and re-creates the columns if freeze is off.

Ship the framework version that contains this flag before (or in the same window as) ALTER TABLE ... DROP of those columns. Never drop them while update() still assigns the properties.

CRUD list UI registers the four fields only when the table’s model has audit enabled (BeanHelper::isCurrentTableEnabledAuditFields()). Unmapped CRUD tables keep the current audit widgets.