Skip to content

Instantly share code, notes, and snippets.

@ozh
Created May 31, 2026 13:22
Show Gist options
  • Select an option

  • Save ozh/c242e07d6f38081577bf5b5e954fd927 to your computer and use it in GitHub Desktop.

Select an option

Save ozh/c242e07d6f38081577bf5b5e954fd927 to your computer and use it in GitHub Desktop.
CLAUDE.md for YOURLS

CLAUDE.md

Instructions for Claude Code working on the YOURLS codebase.

YOURLS (Your Own URL Shortener) is a self-hosted, open-source PHP URL shortener. Primary target: shared hosting (no root/sudo), MySQL 5.5+, PHP 8.1+. WordPress-inspired architecture: hook/filter system, options table, plugin API.

General guidelines

  • Never commit.
  • All functions are prefixed yourls_ + snake_case description, e.g. yourls_do_this_task.
  • Use docblocks (@param/@return) for functions, classes, and complex logic. Comment only when not self-explanatory.
  • Edit only code relevant to the task. No formatting/whitespace-only changes.
  • Don't break shared-hosting compatibility (no root, MySQL 5.5+).
  • Full function/hook/filter reference: see database-and-function-reference.md.

Architecture

Entry points

  • yourls-loader.php - front-facing redirect engine (resolves short URLs)
  • yourls-api.php - REST API endpoint
  • admin/ - admin interface (requires auth); admin/admin-ajax.php - AJAX handler
  • yourls-go.php - redirect handler (GO mode)
  • yourls-infos.php - stats page for a short URL (append +)

Bootstrap flow

yourls-loader.php -> includes/load-yourls.php -> YOURLS\Config\Config (finds/loads config) -> YOURLS\Config\Init (orchestrates startup) -> procedural core.

YOURLS\Config\InitDefaults defines a boolean flag per startup step, so tests can skip steps (DB connect, plugin load, ...). See tests/bootstrap.php.

Code structure: mixed OOP + procedural

Thin OOP layer under includes/ (PSR-4 as YOURLS\) + large procedural core:

  • includes/functions-*.php - procedural functions, all yourls_-prefixed
  • includes/Config/ - Config, Init, InitDefaults (bootstrap)
  • includes/Database/ - YDB, Options, Logger, Profiler
  • includes/Views/AdminParams.php - admin view helpers
  • admin/ - admin UI pages
  • user/ - user config + plugins
  • includes/vendor/ - vendored deps (Aura\Sql, etc.)

YDB extends Aura\Sql ExtendedPdo: the global $ydb god-object (DB, options, context, installed state).

Hook/filter system

WordPress-style, central to everything. Full hook list: database-and-function-reference.md.

  • Filters (modify + return a value): yourls_add_filter, yourls_apply_filter, yourls_has_filter, yourls_remove_filter.
  • Actions (side effects): yourls_add_action, yourls_do_action, yourls_has_action, yourls_did_action (returns a count).
  • Signature: yourls_add_filter('hook', 'callback', $priority = 10, $accepted_args = 1); apply with yourls_apply_filter('hook', $value, $extra_arg1, ...).

Shunt pattern

Many core functions check a shunt_<functionname> filter first, letting plugins bypass core logic and return their own value. Sentinel is yourls_shunt_default(); if the filter returns anything else, the function short-circuits.

$pre = yourls_apply_filter('shunt_add_new_link', false, $url, $keyword, $title);
if (false !== $pre) {
    return $pre;
}
// ... normal logic

Plugins live in user/plugins/. The plugins_loaded action fires once all plugins are loaded.

Database

YOURLS\Database\YDB extends Aura.SQL ExtendedPdo; all queries go through it.

  • yourls_get_db() returns the YDB singleton.
  • Query methods: standard PDO + Aura.SQL fetchAll(), fetchOne(), fetchCol(), fetchPairs(), fetchValue(), fetchAffected().

Three tables (accessed via constants):

YOURLS_DB_TABLE_URL - keyword->URL mappings

  • PK keyword varchar(100); url text, title text, timestamp timestamp, ip varchar(41), clicks int(10)
  • Indexes: ip, timestamp, url_idx (url prefix 30)

YOURLS_DB_TABLE_OPTIONS - key/value settings

  • PK option_id bigint auto_increment; option_name varchar(64), option_value longtext
  • Index: option_name

YOURLS_DB_TABLE_LOG - clicks + referrers

  • PK click_id int auto_increment; click_time datetime, shorturl varchar(100), referrer varchar(200), user_agent varchar(255), ip_address varchar(41), country_code char(2)
  • Index: shorturl

Key constants

User-defined in user/config.php: YOURLS_DB_USER/PASS/NAME/HOST/PREFIX, YOURLS_SITE (base URL), YOURLS_COOKIEKEY (cookie hashing secret), YOURLS_URL_CONVERT (36 or 62), YOURLS_PRIVATE, YOURLS_UNIQUE_URLS, YOURLS_DEBUG.

Core: YOURLS_ABSPATH, YOURLS_INC, YOURLS_USERDIR/YOURLS_USERURL, YOURLS_PLUGINDIR/YOURLS_PLUGINURL, YOURLS_VERSION, YOURLS_DB_VERSION.

Testing

Tests run in Docker against a real DB (no mocking):

docker exec -it dev-web bash --login
cd /var/www/html/ozh.in
phpunit                                   # all
phpunit tests/tests/format/FormatTest.php # one file
phpunit --group formatting                # one group

Code style

  • Opening brace on same line: function foo(string $x, int $y) : bool {
  • Comments in English; docblocks (@param/@return) for classes/functions
  • End multi-line scripts with a blank line
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment