ATmosphere PHP Conventions

SkillFiles & storage

PHP coding standards and WordPress patterns for ATmosphere plugin. Use when writing PHP code, creating classes, implementing WordPress hooks, or structuring plugin files.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the ATmosphere PHP Conventions skill

What this skill tells your AI

The instructions your AI receives, as published by automattic/wordpress-atmosphere in .agents/skills/code-style/SKILL.md and read by ahel’s review.

Quick-reference for everyday work. Full reference: docs/php-coding-standards.md and docs/php-class-structure.md.

Non-Negotiables

  • Text domain: always 'atmosphere'.
  • Tabs for indentation, spaces inside parentheses, array() (not []).
  • Backslash-prefix WordPress and PHP global functions in namespaced code: \get_option(), \add_action(), \apply_filters(), \strlen().
  • Use imports for cross-namespace references — never inline \Atmosphere\OAuth\Client.
  • Yoda conditions for value-vs-variable comparisons: if ( 'value' === $variable ).
  • unreleased for @since / @deprecated tags on new code — the release script rewrites them.
  • Never call \error_log() directly — route every log line through Atmosphere\debug_log(), which gates on WP_DEBUG, adds the [atmosphere] prefix, and collapses CRLF. Pass the message without the prefix.

File and Class Layout

includes/
├── class-*.php              # Atmosphere, API, Publisher, Backfill, Handle, Post_Types, Reaction_Sync, Autoloader.
├── functions.php
├── content-parser/          # Registry, base, and parsers for site.standard.document content.
├── oauth/                   # Client, DPoP, Encryption, Nonce_Storage, Resolver.
├── transformer/             # Post, Document, Publication, Comment, Facet, TID (extend Base).
└── wp-admin/                # Admin UI.
integrations/                # Plugin-specific content-parser integrations.

After adding or renaming a class file under the Atmosphere namespace, no Composer autoload step is needed; runtime loading uses includes/class-autoloader.php.

Transformer Pattern

namespace Atmosphere\Transformer;

class Custom extends Base {
    public function transform(): array {
        // Build the AT Protocol record array.
    }

    public function get_collection(): string {
        return 'app.custom.collection';
    }

    public function get_rkey(): string {
        // Reserve or return the TID; persist to META_TID so it survives retries.
    }
}

Always reserve the rkey via meta in get_rkey() — that meta key is the marker Publisher::update_post() uses to distinguish "never published" from "publish attempt failed mid-flight."

Hook Quick-Reference

Transform filters: atmosphere_transform_bsky_post, atmosphere_transform_comment, atmosphere_transform_document, atmosphere_transform_publication, atmosphere_transform_threadgate.

Records: atmosphere_record_tags (tag/keyword list for both record types; runs before the 8-tag cap, so prefer it over the record-level transform filters for tag changes).

Content / composition: atmosphere_content_parser (deprecated; use Content_Parser\Registry::register()), atmosphere_document_content, atmosphere_long_form_composition, atmosphere_teaser_thread_posts.

Gating: atmosphere_syncable_post_types, atmosphere_should_publish_comment, atmosphere_should_publish_bluesky_post (return false for document-only publishing — no app.bsky.feed.post companion; pure filter, forward-only), atmosphere_should_sync_reply, atmosphere_backfill_query_chunk_size, atmosphere_oauth_redirect_uri, atmosphere_client_metadata, atmosphere_publish_retry_delays (backoff ladder for failed publish/update cron workers; length = retry budget; empty array disables retries).

Actions: atmosphere_publishing, atmosphere_publish_post_result, atmosphere_publish_comment_result, atmosphere_update_skipped_unsynced_post, atmosphere_long_form_strategy_downgraded, atmosphere_reaction_synced. atmosphere_publishing receives the current WP_Post and is not a request-wide guard.

Logging: atmosphere_debug_log(bool $enabled, string $message); defaults to the WP_DEBUG state, lets operators opt log lines in/out independently of WP_DEBUG.

Test-only: atmosphere_pre_apply_writes / atmosphere_pre_get_record — Publisher fixture uses these to short-circuit apply_writes / get_record before the HTTP layer.

Full signatures and docblocks: docs/php-coding-standards.md → Hook Patterns.

Post Visibility and Federation Cleanup

Federation output is remote, site-wide state. Treat a post as publishable only when it is publish, its post type is supported, and post_password is empty. Do not use post_password_required() for AT Protocol records: it depends on the current visitor's unlock cookie and can leak protected content into PDS records.

When a previously-published post leaves public visibility (draft, pending, private, trash, custom non-public status, password applied, or post type support removed), delete remote records rather than updating them with redacted content. Status transitions may queue the normal delete event, but stale publish/update cron handlers must re-check visibility at fire time and call Publisher::delete_post( $post ) directly when local record metadata exists.

Cron Lifecycle — Three-Way Symmetry

Every plugin-owned wp_schedule_* hook MUST appear in Atmosphere\get_cron_hooks() (includes/functions.php). That list drives:

  • Atmosphere\deactivate() (atmosphere.php)
  • Atmosphere\OAuth\Client::disconnect()
  • uninstall.php

When adding a new cron hook:

  1. Add it to get_cron_hooks() — never duplicate the literal in deactivate / disconnect / uninstall.
  2. If the handler issues PDS writes without re-checking is_connected(), the symmetry is load-bearing: a queued event from a previous connection would otherwise fire against a different repo on reconnect.
  3. If the handler stores or sweeps post/comment meta keys, mirror those keys in uninstall.php.

This pattern was extracted in PR #32 (review by @kraftbj). Full rationale: docs/php-coding-standards.md → Cron-Specific Rules.

Cron Handler Errors — Never Swallow WP_Error

Cron handlers in register_async_hooks() MUST surface Publisher::* errors via debug_log() — typically through log_cron_error(). wp_schedule_single_event does not retry, so a silent drop loses the only signal operators have for transient PDS failures, expired refresh tokens, or DPoP nonce drift. debug_log() gates the line behind WP_DEBUG (operators can opt in on production via the atmosphere_debug_log filter).

When the handler operates on records the caller has already lost local state for (e.g. atmosphere_delete_comment_record after the WP comment row is gone), include the TID/identifier in the log line so the orphan is recoverable manually.

Inflight-State Races

When a cron handler writes meta both before an apply_writes call (e.g. Comment::get_rkey() persists META_TID) and after (e.g. store_comment_result() writes META_URI), and a concurrent state change can short-circuit the cleanup gates that key off the post-call meta, the handler MUST re-check eligibility after the call returns and roll back if needed.

Concrete pattern: atmosphere_publish_commentreconcile_comment_after_publish(). Re-fetch the WP object, re-run the eligibility gate, schedule the orphan-cleanup cron (not direct delete) so transient PDS failures retry through the standard channel.

When to Read the Full Docs

Signals

GitHub stars
52
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
code-style-automattic
Source
github.com/automattic/wordpress-atmosphere