Release/2.1.0 - WIP#119
Closed
JJJ wants to merge 183 commits into
Closed
Conversation
* Fix return types in Query * Docs: improvents recommended by phpstan-wordpress. Props szepeviktor. Co-authored-by: Viktor Szépe <viktor@szepe.net>
This change ensures that, going forward, this methods parameter signature better matches the other _item() methods. This should be /relatively/ safe to do thanks to it being private. I've looked at all of the projects I'm aware of that use Berlin, and they are all uneffected.
This refactors the Table::create() SQL generator to explode an array of parts, which should make it easier to make more edits to later.
* Column: improvements to $pattern * Always a string (no false) * Add link to PHP docs * Update description to remove "string replace" * Set UUID to %s * Base: prevent double prefixing in apply_prefix() * Column: correct inline doc * Column: rename private references from pattern to format. * Autoloader: minor code cleanup * Column: various improvements: * Tons of inline & block docs * Smarter default class values * Column::args are saved during parse_args() for later reuse * Prefer get_object_vars() over another array of args * Add "extra" support to special_args() * Add is_ methods for some other types * Add is_extra() method for comparing extra values * Add sanitize_extra() for allowing specific values * Improve fallback support in sanitize_pattern() * Improve fallback support in sanitize_validation() * Remove function_exists check for gmdate() from validate_datetime() * Legitimize validate_numeric() and use where appropriate * Improve get_create_string() with support for binary, more types, null, etc... * Base: introduce sanitize_column_name() * Allows upper-case letters in table and column names. * Swaps out sanitize_key() usage for a custom preg_replace: '/^[a-zA-Z0-9_\-]+$/' * Table: use Base::sanitize_column_name() * Also fix return value inline comment in count() * Column: introduce validate() and validate_int() * validate() centralizes column value validation into the most logical location, and validate_int() allows for falling back to $default in a way that intval obviously could not. * Base: update regex. * Base: add stash_args() method * Also avoid errors in apply_prefix() if not a string. * Column: use stash_args() * Also bail early if no arguments to parse. * Schema: add support for indexes. * Move filters into methods and their own section * Improve docs, and add missing docs * Default values for item_names to prevent fatals * Add setup() method and move set_ methods out of __construct() and into it * Minimize touches to this->columns for future Schema/Structure work * Remove assumptions that primary column must be an int that uses absint/intval - see #124 * Add some todo's for MySQL 8 improvements * Improve support for CURRENT_TIMESTAMP in relevant columns * Add get_columns_field_by() to retrieve a single field from a matching array of values to a single key - primarily used for getting an array of column patterns when querying, to return an array of formats for sprintf() * Improve readability of do_action_ref_array() calls * Clean-up get_item_ids() * Rename parse_where() to parse_query_vars() * Add parse_where() and parse_join() – likely get renamed in the future * Use wp_parse_list() instead of wp_parse_id_list() - likely needs its own handler * Prevent fatals from return values of get_search_sql() * Abstract repeated code into new get_in_sql() method to escape/prepare/format IN (%s) SQL * Introduce parse_query_var() and use it in place of repeated query_vars[] touches - attempts to internally parse comma separated strings (might remove) * Introduce undocumented $column->by check to allow a column to not be queried directly by its name * Refactor parse_query_vars() to improve its internal patterns, for future abstraction * Fallback in parse_fields() and parse_groupby() to prevent fatal errors * Introduce parse_single_orderby, parse_limits, parse_join, and parse_where - refactor parse_orderby * Some minor clean-up to shape_items() * Bail early in get_item_fields() to avoid trying to filter empty fields * Introduce validate_item_field() to call $column->validate(), and use it inside shape_item_id() and more. This centralizes validation and ensures they always return the same results. * Update add_item() and update_item() to skip database if $save fails validation * Update copy_item() to shape the item ID, as it is not done inside of get_item_raw() * Update delete_item() to match other item changes above * All _item() functions use get_columns_field_by() to get patterns to send into wpdb queries for proper formatting (including delete_all_item_meta) - see #137 * Update validate_item() to use validate_item_field() * Use shape_item_id() in update_item_cache() - also use is_scalar() in place of is_numeric() when making assumptions about the shape of the primary column * Stop shaping the ID inside of get_non_cached_ids(), as item IDs are (or should be) previously shaped * Gut get_results() and make it use the query() method - this needs more work * Query: Introduce filter_search_columns() * Switch it to using apply_filters_ref_array() - minor back-compat break * Add direct Schema support * Get columns directly from Schema * Deprecate $columns var * Introduce set_query_clause_defaults() for allowing the query & request clauses to be updated easier * Add keys to query & request clauses * Refactor the way that counts & searches are parsed * Always include columns when count & groupby are used together * Pass query_vars into more parse_ methods to further abstract their usages for future un-privating * Override query_vars in parse_query() when counting * Introduce parse_count() * Rename parse_where/join to _clauses() suffix * Pass arguments into default_item(), and use array_combine() * Swap some var orders in prime_item_caches() * All: update @copyright and README
* Remove $columns * Improve query parsing to allow reuse for second COUNT(*) query_clause overrides * Add support for SELECT & EXPLAIN clauses * Add several new methods to abstract out newly repeated behaviors * Add is_valid_column() and get_query_var() and get_column_name_alias() to help with repeated code patterns * Add parse_query_vars() again, to help abstract only the parsing part * Use get_meta_type() when updating meta data * Update prime_item_caches() to not bail early so it can continue on and try updating meta data
* Base: minor refactor to magic methods, and is_success() * Query: MySQL 8 support * Remove $columns * Improve query parsing to allow reuse for second COUNT(*) query_clause overrides * Add support for SELECT & EXPLAIN clauses * Add several new methods to abstract out newly repeated behaviors * Add is_valid_column() and get_query_var() and get_column_name_alias() to help with repeated code patterns * Add parse_query_vars() again, to help abstract only the parsing part * Use get_meta_type() when updating meta data * Update prime_item_caches() to not bail early so it can continue on and try updating meta data
* Resolve PHPStan Level 0 errors * Fix boolean handling * Revert change for get_sql * Make $index public
This ends up being easier to understand than following the code backwards.
Also use __NAMESPACE__
* Query: Add support for query handlers. * Break apart parse_where() into multiple methods * These new methods return an array of where & join clauses * Those clauses get merged together to maintain backwards compatibility * Query: normalize Join/Where order. * Query: graduate "join" to first-class clause. * Schema: these need to stay protected * Query: remove array_flip from get_column_names * Query: docs * All: improve code consistency * Bail early with specific (non retval) values * Improve inline docs * Remove some unuseful references
Contributor
|
Once this was a gift for a WP plugin developer: https://gist.github.com/szepeviktor/ddb1bfd12d93accd318cc081637956ec |
…ly correct - Query: drop `is_string($item_shape) &&` (get_current_string guarantees string|null) - Query: simplify `is_scalar($found_items_val) ? (int)... : 0` → `(int)$found_items_val` - Meta: drop both `is_scalar($type)` ternaries (type is already a valid string at those points) - In/NotIn/By: replace three is_string two-liners per file with (string) casts at call site - Search: same for get_quoted_column_name_aliased and get_item_name_plural callers - Lifecycle: add get_current_string(), get_current_array(), get_current_int() typed accessors No behaviour change — these guards all handled conditions that cannot occur in the established call paths. Casting at the call site makes the type contract explicit without adding defensive middleware.
PHPCS now exits 0 with zero errors and zero warnings across all 42 source files. PHPStan level 8 remains clean. Excluded from phpcs.xml (intentional deviations): - WordPress.Files.FileName.*: PSR-4 PascalCase filenames - WordPress.Arrays.ArrayKeySpacingRestrictions: $arr[ 'key' ] spacing style - Squiz.Commenting.FunctionComment.IncorrectTypeHint: PHPDoc uses list<T> / array<K,V> generics; PHP runtime type hint is always array - Squiz.PHP.CommentedOutCode: too many false positives on PHPStan @var annotations, MySQL keyword labels, and URL comments Key changes: - Restored $arr[ 'key' ] bracket spacing across all src/ files (phpcbf had stripped it during earlier auto-fix passes) - Added proper short descriptions to property docblocks in all 17 operator and 7 parser concrete subclasses - Changed inline /** @var Type $var */ type assertions to use phpcs:ignore MissingShort (must stay as /** */ — PHPStan only narrows on doc comments) - Added @param descriptions to 87 bare @param lines; added trailing periods to 20 param descriptions missing them - Replaced mt_rand() with wp_rand() in Column.php UUID generation - Added phpcs:ignore for MySQL-returned properties (Msg_text, Checksum) in Table.php that cannot be renamed - Collapsed multi-line ternaries to single lines where phpcs:ignore was needed - Capitalized long/short descriptions; added periods to inline comments; fixed block comment endings and spacing issues
…_cache() $last_changed was the last ephemeral property not consolidated into $current[]. With the old EDD/SC guard already removed (fix for #160), the property served no cross-call purpose — set_last_changed() was called and read back within the same two lines of update_last_changed_cache(). Inline it as a local variable and delete the property and method entirely. Adds QueryCacheTest::test_cache_is_invalidated_after_delete to cover the #160 regression scenario: delete an item, re-query on the same instance, expect an empty result rather than the stale cached row.
…ion hooks
Five tests across three files, all documenting previously uncovered behaviour:
- QueryCrudTest: get_item_by() returns false for '0' and 0 column values —
documents the empty($column_value) guard (empty('0') and empty(0) are both
true in PHP, so these bail before the database regardless of table contents)
- QueryFilterTest: orderby => '' falls back to the primary column via
parse_single_orderby() → get_primary_column_name(), returns a non-empty
result set in ID order
- QueryTransitionTest (new): transition hook fires on add_item() with 'new'
as old_value (WordPress new-item convention); fires on update_item() when
status changes; does not fire when status is unchanged (array_diff bail)
copy_item() passed the full raw row to add_item() without stripping the UUID.
Column::validate_uuid() preserves any existing valid UUID unchanged ("UUIDs
should never change once they are set"), so every copy silently inherited the
original's UUID — two rows, one identifier.
Fix: unset 'uuid' from $save before the $data override merge. This lets
add_item() generate a fresh UUID via validate_uuid(). A UUID explicitly
provided in the $data override array is still respected (restored by the merge).
Adds test_copy_item_generates_distinct_uuid() to QueryCrudTest to pin the
corrected behaviour and catch any regression.
Introduce a small Log trait for structured in-memory diagnostic logging without adding another WordPress-specific dependency. The trait provides protected log collection, public log access/clearing helpers, and a no-op write_log() bridge for projects that want to forward entries to debug.log, error_log(), Monolog, Query Monitor, or another writer. Compose Log into the shared Base trait so BerlinDB core objects can use it consistently, and add focused tests covering stored entries, level filtering, clearing, writer bridging, and empty-entry guards. (Also update Trait doc-block text to be shorter)
write_log() is always called with a real entry — the array $entry = array() default implied it could meaningfully be called with no args, which it cannot. Remove the default from both the trait and the LogTestSubject override. Add three tests that were absent from the initial pass: - get_logs() returns [] before any entries are recorded - entries are returned in insertion order - clear_logs($level) re-indexes remaining entries so keys stay sequential
…, call sites Adds a machine-readable $code field to every log entry so callers and readers can match on stable event identifiers independently of message text. get_logs() and clear_logs() now accept an $args field/value filter and a 'and'/'or' operator, backed by the new protected filter_logs() and log_matches() helpers (both now carry explicit return type hints). Adds four semantic log helpers that build consistent entries from common diagnostic scenarios: - log_empty_value() - log_class_not_found() - log_class_instantiation_failed() - log_method_not_found() Each helper accepts an optional $caller array (callable-style [object, method]) and a $context override array whose special keys (code, message, level) promote to the entry top level so callers can customise without rebuilding the full entry. Supporting internals: get_log_code(), get_log_caller_context(), get_log_class_short_name(), normalize_log_key(). First real call sites land in Query::set_schema() and Table::set_schema(), covering the four failure modes (empty schema, class not found, instantiation failure, missing required method) with stable event codes (query_schema_* / table_schema_*). Downstream callers already guard with is_callable() so a partial schema object does not crash; the log entry surfaces the misconfiguration for debugging without aborting the boot sequence. LogTest expanded to 10 assertions covering the new $code field, AND/OR field filtering, clear_logs() re-indexing, write_log() bridge, and empty-input guards. QuerySchemaLogTest and TableSchemaLogTest added to verify each set_schema() failure path records the correct log code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a lightweight Log trait that gives every BerlinDB kernel
class (Query, Table, Column, Schema, Index, Row) a shared, structured
in-memory log. Entries carry a stable machine-readable code, level,
human-readable message, key-value context, timestamp, and source class.
Core API:
- log() — protected; normalises level/code, trims message, guards
empties, calls the write_log() hook so subclasses can bridge to any
external log destination without overriding the storage logic.
- get_logs( $args, $operator ) — returns all entries or those matching
field/value pairs in AND (default) or OR mode.
- clear_logs( $args, $operator ) — removes matching entries (or all)
and re-indexes the remaining array.
- write_log( $entry ) — no-op hook; override to forward to error_log(),
Monolog, Query Monitor, etc.
filter_logs() / log_matches() carry explicit :array / :bool return
types and the full entry shape in their docblocks so PHPStan can track
the specific array shape through filtering without widening it.
First real call sites land in Query::set_schema() and
Table::set_schema(), each logging a single structured entry under the
stable code query_schema_unavailable / table_schema_unavailable when
the schema class is empty, missing, throws on construction, or lacks
the required method. Downstream callers already guard with is_callable()
so a partial schema object does not abort the boot sequence; the log
entry surfaces the misconfiguration for debugging.
Tests: LogTest (10), QuerySchemaLogTest (3), TableSchemaLogTest (3).
phpcs.xml updated from WordPress to WordPress-Core to align with
phpcs.xml.dist, and with the standard exclusion set from that baseline:
- Generic.Files.OneObjectStructurePerFile excluded for tests/
(multiple helper classes per file is the normal test pattern)
- WordPress.DB.DirectDatabaseQuery / SlowDBQuery excluded for tests/
(integration tests intentionally issue direct queries)
- Squiz.Commenting.FunctionComment, DocComment.ShortNotCapital,
DocComment.LongNotCapital, and related noise sniffs excluded globally
(consistent with phpcs.xml.dist and eliminates ~150 pre-existing
false positives in the test suite that were blocking CI)
PHPStan level 8: 0 errors. PHPCS: 0 errors. PHPUnit: 498/498.
Closes #153
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add coverage for PHP's same-family protected property behavior, where a subclass can read a protected property from another object in the same inheritance tree without invoking __get(). Also add Query-specific coverage showing the practical BerlinDB result: external table_name access resolves through get_table_name(), while sibling Query subclass access reads the raw configured/prefixed property value directly. Refs #46
Phase 1 — Schema object injection (Query + Table) $table_schema on Query and $schema on Table now accept either a class name string (the existing subclass pattern, unchanged) or a live Schema instance. set_schema() checks instanceof first and short-circuits; the existing string path is untouched. This makes both Boot-based constructor injection and direct property assignment work: new Query( array( 'table_schema' => $schema ) ) new Table( array( 'schema' => $schema ) ) No existing subclass is affected — the instanceof guard only fires when a Schema object is present. Phase 2 — MySQL introspection factories Column::from_mysql( array $row ) Maps a single SHOW COLUMNS row (Field, Type, Null, Key, Default, Extra) to a fully constructed Column. The type string is parsed with a single regex into base_type, length, unsigned, and zerofill. Null, Key, and Default map to allow_null, primary, and default. Passing empty strings for pattern, cast, and validate triggers the existing auto-inference in sanitize_pattern(), sanitize_cast(), and sanitize_validation(), so the correct cast and format are inferred from the column type at construction time. Schema::from_table( string $table ) Queries SHOW COLUMNS FROM $table via the get_db() wrapper (no global $wpdb), maps each row through Column::from_mysql(), and returns a ready Schema. Returns an empty Schema if the table does not exist or the DB interface is unavailable. Together these close the loop: a live MySQL table can now be reverse- engineered into a working Query in a few lines: $schema = Schema::from_table( $wpdb->prefix . 'posts' ); $query = new Query( array( 'table_schema' => $schema ) ); PHPStan level 8: 0 errors. PHPCS: 0 errors. PHPUnit: 500/500. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two new test classes covering the MySQL introspection factories introduced in the previous commit. ColumnFromMysqlTest (33 tests) exercises from_mysql() with hand-crafted SHOW COLUMNS rows and documents all sanitization side-effects — type and extra are normalised to uppercase, string defaults collapse to '' due to the validate-callback ordering constraint in sanitize_args(). SchemaFromTableTest (27 integration tests) issues live SHOW COLUMNS queries against wp_posts and wp_users and asserts column count, type, primary-key, date_query, and ordering invariants that hold across all supported WordPress versions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Column::sanitize_default() was discarding string defaults because it called validate($fallback) with only one argument, leaving the method's own $fallback parameter at its empty-string default. Passing the value in both positions — validate($value, $value) — preserves it when no validate callback is registered at construction time (as is the case during from_mysql() introspection). Environment::get_db_global() is a new static accessor that holds the DB resolution logic; get_db() now delegates to it instead of duplicating it. Schema::from_table() uses the static accessor directly (eliminating the throwaway-instance smell) and wraps the SHOW COLUMNS query in suppress_errors() so a missing table silently returns an empty Schema rather than printing an HTML error block to the page. Test suite: ColumnFromMysqlTest now builds shared column fixtures in setUpBeforeClass() instead of repeating the same six-key array nine times; the varchar-default assertion is corrected to 'publish' and the missing-key assertion is corrected to false; SchemaFromTableTest drops the manual suppress_errors() wrappers that are no longer needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Abstracts the database contract behind a Connection interface (Interfaces/Connection.php) with a Wpdb adapter wrapping \wpdb and a NullConnection adapter implementing the Null Object pattern. The Environment trait now always returns a Connection — never false — so the ~50 empty($db) early-return guards scattered across Query, Table, Schema, Parsers, Operators, and Traits were all dead code and have been removed. NullConnection::get_table_prefix() is a pass-through, which preserves the pre-guard fallback behaviour in get_table_name() at zero cost. PHPStan level 8 clean, PHPCS clean, 560/560 tests passing.
get_db_global() now caches the Wpdb adapter in a static array keyed by global name. spl_object_id() guards the cache entry so a replaced $wpdb instance (e.g. in tests) correctly yields a fresh adapter rather than reusing the stale one. Because the adapter holds a reference to the underlying \wpdb object rather than a snapshot, prefix mutations from switch_to_blog() are visible through the cached adapter with no extra work — the seven new EnvironmentTest cases cover this explicitly, including a direct assertion that $wpdb->prefix mutations are reflected after a cache hit.
Renames the protected accessor on the Environment trait from get_db() to db() — shorter and consistent with non-get_ accessor conventions. All 48 call sites are updated: the "// Get the database interface." / "$db = $this->db();" two-line preamble is removed and uses are inlined as $this->db()->method(). A deprecated get_db() alias is retained on the trait so existing subclasses (EDD, Sugar Calendar, etc.) continue to compile without changes. Two justified exceptions remain: Schema::from_table() is static so it uses $db = self::get_db_global(), and Search::get_search_sql() keeps a local $db because array_map requires an object reference for its callable argument. Static adapter cache makes every inlined call a single array lookup at no meaningful cost. Fixes #69.
Closes #139. Adds 'cache_results' (default true) to query_var_defaults, mirroring WP_Query/WP_Term_Query's pattern. When false, the result-list cache is neither read nor written for that query, giving callers a per-query escape hatch without resorting to wp_cache_flush(). The var is excluded from cache key generation so cached and uncached calls for the same args share the same key slot.
NullConnectionTest (PHPUnit\TestCase): verifies all Connection interface methods return inert/safe values and that set_table_prefix/register_table are no-ops. WpdbTest (WPIntegration\TestCase): verifies delegation to the live $wpdb instance for prepare, esc_like, property accessors, and the dynamic table-prefix registry, with state restoration after each case. QueryGetResultsTest (PHPUnit\TestCase): uses a query subject that captures args without a database to cover the three arg-mapping paths in get_results(). ErrorTest (PHPUnit\TestCase): data-provider-driven coverage of is_success() for all failure sentinels (false, null, 0), positive values, and the WP_Error stash.
Use ALTER TABLE ... RENAME TO for Table::rename() so wpdb treats the operation as DDL and returns a usable success value. Also prevent Boot::parse_args() from restoring its internal args stash through set_vars() when merging constructor defaults. Add coverage for table copy, rename, repair, Boot argument stashing, and existing Query hook smoke tests.
Add direct tests for Parser::get_cast_for_type() and sanitize_query() behavior around invalid numeric children and OR relation tracking. This complements the existing concrete parser integration coverage without changing production code.
Add a CI workflow with separate PHPStan, PHPCS, and PHPUnit jobs for pull requests and pushes to main, trunk, and release branches. Allow the Docker PHPUnit runner to skip its built-in PHPCS step via SKIP_PHPCS so CI job names map cleanly to the work they perform, while preserving local test-runner behavior by default.
Add export-ignore rules so Composer dist archives only include the runtime package files: license, readme, autoloader, composer metadata, and src. Declare PHP 8.1 as the supported minimum, update the Composer platform to 8.1, and refresh the lock file metadata. PHP 8.0 is close at runtime, but the current dev/test dependency stack requires PHP 8.1+. Remove the stale hardcoded package version and add GitHub support links. Expand PHPUnit CI to run on PHP 8.1 and 8.2, and give PHPStan a 1G memory limit for more reliable CI runs. Verified with composer validate, PHPCS, PHPStan, PHPUnit on PHP 8.1 and PHP 8.2, and a Composer archive inspection.
Rewrite the README around installation, requirements, quick-start usage, documentation links, and development workflow. Add changelog, contribution guide, security policy, release checklist, issue templates, pull request template, and markdownlint configuration. Add Packagist-friendly Composer metadata and tighten export-ignore rules so Composer archives stay focused on runtime package files. Add a manual Pre-Release workflow that runs static checks, supported PHPUnit lanes, changelog validation, and Composer archive inspection.
Release/3.0.0 - WIP
Collaborator
Author
|
I'm going to close this PR. Everything in this branch is also in the 3.0.0 branch and tag. Thanks to everyone for helping 💞 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Work in progress!