Skip to content
Developer docs

Contributing and running the tests

Updated 4 min read

Basehim is a deliberately small, dependency-free PHP CMS, and contributions that keep it that way are welcome. This page gets you from a fresh clone to a passing test run and a pull request.

Before you start

  • Bugs and small fixes: open a pull request directly.
  • New features, dependencies or schema changes: open an issue first, so the shape can be agreed before you spend time on it.
  • Security problems: report them privately, never in a public issue. See Security for developers.

Set up a checkout

git clone https://github.com/<your-username>/basehim.git
cd basehim
php -S localhost:8000 server.php      # open http://localhost:8000 and run the installer

Set APP_DEBUG=true in the generated .env while you work. To start over, drop the database and delete .env.

The test suite

The runner and its assertions are a few hundred lines in tests/support/. The suite uses this runner instead of PHPUnit, so it needs only PHP.

php tests/run.php                       # unit tests: no database needed
php tests/run.php --filter=csrf         # only tests whose name contains "csrf"
php tests/run.php --keep                # keep the test database and logs afterwards

Integration tests

Integration tests drive the real index.php over HTTP, with real sessions, the real login form and a real database. They run when you point the runner at a database server:

BASEHIM_TEST_DB_HOST=127.0.0.1 \
BASEHIM_TEST_DB_USER=root BASEHIM_TEST_DB_PASS=secret \
php tests/run.php
VariableDefault
BASEHIM_TEST_DB_HOSTnoneRequired to enable integration tests
BASEHIM_TEST_DB_PORT3306
BASEHIM_TEST_DB_USER, BASEHIM_TEST_DB_PASSroot, empty
BASEHIM_TEST_DB_NAMEbasehim_testMust end in _test: it is dropped and recreated
BASEHIM_TEST_DB_PREFIXbht_Exercises table prefixing

A run creates the database, applies every migration, seeds a user for each role, installs a test app, and serves the site with PHP's built-in server. It never reads your own .env or touches your own database. With Node.js 18+ installed, the JavaScript client tests run too. The fault-isolation and public-route tests install their own broken and example apps under throwaway slugs and remove them afterwards.

CI runs the whole suite against MySQL 8.0 on PHP 8.1 and 8.3, on every push and pull request.

Writing a test

<?php
// tests/Integration/MediaOwnershipTest.php

test("an author cannot rename someone else's image", function () {
    $b = bh_browser('author');                     // signed in through the real login form
    $res = $b->ajax('media.update', ['id' => 1, 'title' => 'x']);
    assert_same(403, $res->status, $res->body);
    assert_same('FORBIDDEN', $res->errorCode());
});

Test files end in Test.php and are found automatically.

  • Tests in tests/Unit/ must not need a database.
  • Tests in tests/Integration/ may use bh_browser($role), bh_db(), bh_env(), bh_core_log() and bh_app_log().
  • Assertions: assert_same, assert_true, assert_false, assert_contains, assert_not_contains and assert_throws. Each takes an optional message.
  • Name a test after what it proves, in a sentence. A failing test should read like a bug report.

Add a test for every bug you fix, and check that it fails without your fix.

Code conventions

  • declare(strict_types=1); at the top of every PHP file.
  • PSR-12, four-space indentation, LF line endings.
  • PSR-4: App\ maps to app/, and the file name matches the class.
  • Classes are final unless they are designed for extension.
  • Repositories do data access, services do business logic, controllers stay thin.
  • A short docblock on every class, and on any method whose behaviour isn't obvious. Explain why, not what.
  • PHP 8.1 is the minimum. Don't use syntax from later versions.

Check syntax before pushing:

find . -name "*.php" -not -path "./vendor/*" -print0 | xargs -0 -n1 php -l

Database changes

Add a new numbered file to database/migrations/. Never edit one that has shipped, because existing sites have already run it.

  • Use {table} tokens so DB_PREFIX works.
  • Make every statement work on MySQL 5.7+ and MariaDB 10.3+.
  • For conditional changes, check information_schema and use PREPARE/EXECUTE instead of MariaDB-only syntax such as ADD COLUMN IF NOT EXISTS.

See Updates, migrations and backups.

Compatibility

Basehim supports managed hosting without shell access, and that shapes what can be accepted:

  • no third-party runtime dependencies, build steps, background processes or shell access at runtime. Where core can use the PHP command line (the app preflight check), it must work without it too;
  • no new PHP extensions beyond the ones the installer checks;
  • public behaviour (REST responses, hooks, App methods, theme variables, URLs) changes only with a compatibility plan and a changelog entry;
  • no new public or protected methods on App\Core\App. Apps extend it, so a name an existing app already uses for its own method, with another signature or visibility, is a fatal error that takes that app down on update. New app features go behind an existing accessor, as $this->router()->site() does. tests/Unit/AppBaseClassTest.php fails if the base class grows.

Pull requests

  1. Branch from main: git checkout -b fix/short-description.
  2. Keep the change to one concern.
  3. Run php tests/run.php with a test database, and add tests.
  4. Update the docs if behaviour changed.
  5. Write a commit message that explains the reasoning, in a loose conventional style:
fix(router): resolve subdirectory base path on HEAD requests
feat(api): add cursor pagination to /api/v1/posts
docs(themes): document the widgets.php field format

Discussion

Leave a comment