Skip to content

Add project content processors#84

Open
samdark wants to merge 3 commits into
masterfrom
add-project-shortcodes
Open

Add project content processors#84
samdark wants to merge 3 commits into
masterfrom
add-project-shortcodes

Conversation

@samdark

@samdark samdark commented Jun 13, 2026

Copy link
Copy Markdown
Member

Summary

  • replace the project shortcode-template approach with external project content processors
  • auto-discover content/processors/*.php when processors config is omitted, while processors: false disables them and explicit config uses only the listed files
  • load returned ContentProcessorInterface instances or callables, validate paths stay inside the content directory, include processor files in build invalidation, and document the feature

Tests

  • make test -- --filter BuildCommandTest::testBuildAppliesProjectProcessors
  • make test -- --filter ProjectProcessorLoaderTest
  • make test -- --filter SiteConfigParserTest
  • make psalm
  • make test
  • BENCH_FILTER=ProjectProcessorBench make bench

Copilot AI review requested due to automatic review settings June 13, 2026 10:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@samdark, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 40 minutes and 1 second. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 11814344-0fd3-4618-9644-f6ea696c693b

📥 Commits

Reviewing files that changed from the base of the PR and between f0c5e96 and c4c2eec.

📒 Files selected for processing (16)
  • benchmarks/ProjectProcessorBench.php
  • docs/configuration.md
  • docs/plugins.md
  • roadmap.md
  • src/Console/BuildCommand.php
  • src/Content/Model/ProcessorConfig.php
  • src/Content/Model/SiteConfig.php
  • src/Content/Parser/SiteConfigParser.php
  • src/Processor/CallbackContentProcessor.php
  • src/Processor/ContentProcessorPipeline.php
  • src/Processor/ProjectProcessorLoader.php
  • src/Processor/ProjectProcessorSet.php
  • tests/Unit/Console/BuildCommandTest.php
  • tests/Unit/Content/Parser/SiteConfigParserTest.php
  • tests/Unit/Processor/ContentProcessorPipelineTest.php
  • tests/Unit/Processor/ProjectProcessorLoaderTest.php
📝 Walkthrough

Walkthrough

Adds a project-level content processor system for static-binary users. Introduces ProcessorConfig, ProjectProcessorSet, CallbackContentProcessor, and ProjectProcessorLoader to discover and load PHP processors from a project-root processors/ directory. Extends ContentProcessorPipeline with insertBefore/insertAfter methods, wires processors into build pipelines via BuildCommand, parses the new processors YAML key in SiteConfigParser, and adds tests, benchmarks, and documentation.

Changes

Project-Level Content Processors

Layer / File(s) Summary
Config data shapes and callback wrapper
src/Content/Model/ProcessorConfig.php, src/Processor/ProjectProcessorSet.php, src/Content/Model/SiteConfig.php, src/Processor/CallbackContentProcessor.php
ProcessorConfig holds discovery flag and before/after-markdown path lists; ProjectProcessorSet bundles four readonly processor-list properties; SiteConfig gains a processors property; CallbackContentProcessor wraps callables as ContentProcessorInterface.
ContentProcessorPipeline insert-before/after API
src/Processor/ContentProcessorPipeline.php
Adds insertBefore() and insertAfter() public methods that splice processor instances relative to an existing processor class, appending when the target is absent, backed by a new positionOf() helper.
ProjectProcessorLoader: discovery, path resolution, and loading
src/Processor/ProjectProcessorLoader.php
Discovers processors/*.php files, resolves and validates paths within the project directory, requires and normalizes return values to ContentProcessorInterface (wrapping callables), and throws InvalidContentConfigException for path or return-type violations.
SiteConfigParser: processors YAML parsing
src/Content/Parser/SiteConfigParser.php
Parses the top-level processors key, handling false, null/true, list syntax, and associative content/feed before/after-markdown path objects; normalizes values and constructs ProcessorConfig.
BuildCommand: processor wiring, inventory, and discovery helpers
src/Console/BuildCommand.php
Moves parseSiteConfig() earlier in execute(); calls configureProjectProcessors() to load and insert project processors into both pipelines relative to MarkdownProcessor; tracks processors/ for incremental builds; passes ProcessorConfig to collectSourceInventory(); adds file-discovery and path-resolution helper methods.
Unit and integration tests
tests/Unit/Processor/ContentProcessorPipelineTest.php, tests/Unit/Content/Parser/SiteConfigParserTest.php, tests/Unit/Processor/ProjectProcessorLoaderTest.php, tests/Unit/Console/BuildCommandTest.php
Covers pipeline insert ordering, YAML parsing (structured paths and processors: false), loader auto-discovery, configured paths, outside-project and invalid-return rejection, and an end-to-end build test verifying processor HTML transformation.
Benchmark, documentation, and roadmap
benchmarks/ProjectProcessorBench.php, docs/plugins.md, docs/configuration.md, roadmap.md
PhpBench benchmark times pipeline processing through a project processor; plugins.md documents discovery, pipeline placement, return types, and config options; configuration.md adds the processors field entry; roadmap marks feature complete.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI / BuildCommand
    participant Parser as SiteConfigParser
    participant Loader as ProjectProcessorLoader
    participant Pipeline as ContentProcessorPipeline
    participant FS as Filesystem

    CLI->>Parser: parseSiteConfig(config.yaml)
    Parser-->>CLI: SiteConfig (with ProcessorConfig)

    CLI->>Loader: new ProjectProcessorLoader(projectDir, configPath)
    CLI->>Loader: load(ProcessorConfig)
    Loader->>FS: glob(processors/*.php) if discover=true
    FS-->>Loader: discovered paths
    loop each path
        Loader->>FS: realpath + require path
        FS-->>Loader: ContentProcessorInterface or callable
        Loader->>Loader: wrap callable in CallbackContentProcessor
    end
    Loader-->>CLI: ProjectProcessorSet

    CLI->>Pipeline: insertBefore(MarkdownProcessor, contentBeforeMarkdown[])
    CLI->>Pipeline: insertAfter(MarkdownProcessor, contentAfterMarkdown[])
    Note over CLI,Pipeline: Same pattern for feed pipeline

    Pipeline-->>CLI: ready for build
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hoppin' through the processors/ lane,
Where PHP files bring custom refrain,
insertBefore, insertAfter — what grace!
Discovery sorts each file into place,
The pipeline gleams, the badge shines bright,
A rabbit's best build, delivered just right! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add project content processors' accurately and concisely describes the main change in the pull request, matching the primary objective of implementing a flexible processor-based system for content transformation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-project-shortcodes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/Processor/Shortcode/ProjectShortcodeProcessor.php (1)

120-120: ⚡ Quick win

Consider documenting the === 1 return value convention.

Line 120's logic $result === 1 ? $output : $output . (string) $result relies on a subtle PHP behavior: when a require statement has no explicit return, it returns 1. This allows templates to use either echo (captured via output buffering) or return (appended to output), but the exact-equality check against 1 is not self-documenting.

Consider adding an inline comment explaining this convention, or documenting it in the class-level docblock.

📝 Suggested inline comment
-        return $result === 1 ? $output : $output . (string) $result;
+        // require returns 1 when template has no explicit return; use only buffered output in that case
+        return $result === 1 ? $output : $output . (string) $result;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Processor/Shortcode/ProjectShortcodeProcessor.php` at line 120, The
ternary in ProjectShortcodeProcessor method returning "$result === 1 ? $output :
$output . (string) $result" relies on PHP's require returning 1 when no explicit
return is used; add a short inline comment at that line explaining that
"require" with no return yields int(1) so the code distinguishes between echoed
output (buffered) and returned string, and also update the class-level docblock
of ProjectShortcodeProcessor to document this convention for template authors
(mentioning that templates may either echo or return content and that a raw
integer 1 means "no return").
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/Processor/Shortcode/ProjectShortcodeProcessor.php`:
- Line 120: The ternary in ProjectShortcodeProcessor method returning "$result
=== 1 ? $output : $output . (string) $result" relies on PHP's require returning
1 when no explicit return is used; add a short inline comment at that line
explaining that "require" with no return yields int(1) so the code distinguishes
between echoed output (buffered) and returned string, and also update the
class-level docblock of ProjectShortcodeProcessor to document this convention
for template authors (mentioning that templates may either echo or return
content and that a raw integer 1 means "no return").

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46f5aa16-1f28-4ca6-a28d-ee50f762ced6

📥 Commits

Reviewing files that changed from the base of the PR and between 315ad8d and dbd0a66.

📒 Files selected for processing (6)
  • benchmarks/ProjectShortcodeProcessorBench.php
  • config/common/di/content-pipeline.php
  • docs/plugins.md
  • roadmap.md
  • src/Processor/Shortcode/ProjectShortcodeProcessor.php
  • tests/Unit/Processor/ProjectShortcodeProcessorTest.php

@samdark samdark changed the title Add project shortcode templates Add project content processors Jun 15, 2026
@samdark samdark force-pushed the add-project-shortcodes branch from 6dae34d to f0c5e96 Compare June 15, 2026 19:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/Unit/Processor/ContentProcessorPipelineTest.php (1)

52-80: ⚡ Quick win

Reduce test code duplication by extracting the shared marker processor.

Both testCanInsertProcessorsBeforeMarkerProcessor (lines 54-59) and testCanInsertProcessorsAfterMarkerProcessor (lines 69-74) define identical anonymous marker classes. This duplication violates the DRY principle.

♻️ Refactor to reduce duplication

Extract the marker processor creation to a helper method:

+    private function createMarkerProcessor(): ContentProcessorInterface
+    {
+        return new class implements ContentProcessorInterface {
+            public function process(string $content, Entry $entry): string
+            {
+                return $content . ' [marker]';
+            }
+        };
+    }
+
     public function testCanInsertProcessorsBeforeMarkerProcessor(): void
     {
-        $marker = new class implements ContentProcessorInterface {
-            public function process(string $content, Entry $entry): string
-            {
-                return $content . ' [marker]';
-            }
-        };
+        $marker = $this->createMarkerProcessor();

         $pipeline = new ContentProcessorPipeline($marker);
         $pipeline->insertBefore($marker::class, $this->createProcessor(fn(string $c) => $c . ' [before]'));

         assertSame('start [before] [marker]', $pipeline->process('start', $this->createEntry()));
     }

     public function testCanInsertProcessorsAfterMarkerProcessor(): void
     {
-        $marker = new class implements ContentProcessorInterface {
-            public function process(string $content, Entry $entry): string
-            {
-                return $content . ' [marker]';
-            }
-        };
+        $marker = $this->createMarkerProcessor();

         $pipeline = new ContentProcessorPipeline($marker);
         $pipeline->insertAfter($marker::class, $this->createProcessor(fn(string $c) => $c . ' [after]'));

         assertSame('start [marker] [after]', $pipeline->process('start', $this->createEntry()));
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/Processor/ContentProcessorPipelineTest.php` around lines 52 - 80,
Both testCanInsertProcessorsBeforeMarkerProcessor and
testCanInsertProcessorsAfterMarkerProcessor methods contain identical anonymous
implementations of the marker ContentProcessorInterface. Extract this shared
marker processor creation into a private helper method (e.g.,
createMarkerProcessor) that returns the processor instance, then replace the
duplicate anonymous class instantiations in both test methods with calls to this
helper method to reduce code duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmarks/ProjectProcessorBench.php`:
- Around line 61-63: The benchmark pipeline in the ProjectProcessorBench class
is only initializing ContentProcessorPipeline with contentBeforeMarkdown
processors, which does not reflect real-world usage. According to BuildCommand,
project processors are inserted before and after MarkdownProcessor within a
complete pipeline that includes core processors. Modify the pipeline
initialization to include MarkdownProcessor and core processors alongside the
project processors from contentBeforeMarkdown, ensuring the benchmark measures
realistic end-to-end performance rather than isolated project-processor
overhead.

In `@src/Console/BuildCommand.php`:
- Around line 1518-1521: The $this->contentPipeline and $this->feedPipeline are
injected singleton objects being mutated by insertBefore() and insertAfter()
calls without reset, causing processor accumulation if the BuildCommand instance
is reused. Before the insertBefore and insertAfter calls, either reset both
pipelines by removing existing processors or create fresh pipeline copies to
ensure idempotency. This prevents duplicate processor registration and execution
in scenarios where the BuildCommand is reused across multiple build executions.

In `@src/Processor/CallbackContentProcessor.php`:
- Around line 16-19: The process method in the CallbackContentProcessor class
performs an unsafe string cast on the callback return value without validation.
Add explicit type validation after calling the callback to check if the return
value is actually a string before the cast. If the return value is not a string,
throw an exception with a helpful error message that indicates the callback must
return a string type. This will prevent silent data corruption from null values
converting to empty strings, boolean/integer/float conversions, or fatal errors
from objects without __toString methods.

---

Nitpick comments:
In `@tests/Unit/Processor/ContentProcessorPipelineTest.php`:
- Around line 52-80: Both testCanInsertProcessorsBeforeMarkerProcessor and
testCanInsertProcessorsAfterMarkerProcessor methods contain identical anonymous
implementations of the marker ContentProcessorInterface. Extract this shared
marker processor creation into a private helper method (e.g.,
createMarkerProcessor) that returns the processor instance, then replace the
duplicate anonymous class instantiations in both test methods with calls to this
helper method to reduce code duplication.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5bb4b831-db53-4a0a-8405-5896fda53ecb

📥 Commits

Reviewing files that changed from the base of the PR and between a410618 and f0c5e96.

📒 Files selected for processing (16)
  • benchmarks/ProjectProcessorBench.php
  • docs/configuration.md
  • docs/plugins.md
  • roadmap.md
  • src/Console/BuildCommand.php
  • src/Content/Model/ProcessorConfig.php
  • src/Content/Model/SiteConfig.php
  • src/Content/Parser/SiteConfigParser.php
  • src/Processor/CallbackContentProcessor.php
  • src/Processor/ContentProcessorPipeline.php
  • src/Processor/ProjectProcessorLoader.php
  • src/Processor/ProjectProcessorSet.php
  • tests/Unit/Console/BuildCommandTest.php
  • tests/Unit/Content/Parser/SiteConfigParserTest.php
  • tests/Unit/Processor/ContentProcessorPipelineTest.php
  • tests/Unit/Processor/ProjectProcessorLoaderTest.php
✅ Files skipped from review due to trivial changes (2)
  • docs/configuration.md
  • roadmap.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/plugins.md

Comment thread benchmarks/ProjectProcessorBench.php Outdated
Comment on lines +61 to +63
$processors = (new ProjectProcessorLoader($this->projectDir, $this->projectDir . '/content/config.yaml'))
->load(new ProcessorConfig());
$this->pipeline = new ContentProcessorPipeline(...$processors->contentBeforeMarkdown);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Benchmark pipeline missing MarkdownProcessor and core processors.

The pipeline is initialized with only project processors from contentBeforeMarkdown, but real-world usage (as seen in BuildCommand) inserts project processors before/after MarkdownProcessor within a complete pipeline. This benchmark measures isolated project-processor overhead rather than realistic end-to-end performance where project processors work alongside core processors.

Consider including MarkdownProcessor and other core processors to match actual usage:

-        $this->pipeline = new ContentProcessorPipeline(...$processors->contentBeforeMarkdown);
+        $markdownProcessor = new MarkdownProcessor(/* ... */);
+        $this->pipeline = new ContentProcessorPipeline($markdownProcessor);
+        $this->pipeline->insertBefore(MarkdownProcessor::class, ...$processors->contentBeforeMarkdown);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/ProjectProcessorBench.php` around lines 61 - 63, The benchmark
pipeline in the ProjectProcessorBench class is only initializing
ContentProcessorPipeline with contentBeforeMarkdown processors, which does not
reflect real-world usage. According to BuildCommand, project processors are
inserted before and after MarkdownProcessor within a complete pipeline that
includes core processors. Modify the pipeline initialization to include
MarkdownProcessor and core processors alongside the project processors from
contentBeforeMarkdown, ensuring the benchmark measures realistic end-to-end
performance rather than isolated project-processor overhead.

Comment on lines +1518 to +1521
$this->contentPipeline->insertBefore(MarkdownProcessor::class, ...$processors->contentBeforeMarkdown);
$this->contentPipeline->insertAfter(MarkdownProcessor::class, ...$processors->contentAfterMarkdown);
$this->feedPipeline->insertBefore(MarkdownProcessor::class, ...$processors->feedBeforeMarkdown);
$this->feedPipeline->insertAfter(MarkdownProcessor::class, ...$processors->feedAfterMarkdown);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether BuildCommand or ContentProcessorPipeline has a reset/clone path
# that prevents project processors from accumulating across repeated execute() calls.

rg -n -C3 'new BuildCommand|CommandTester|execute\(|insertBefore\(|insertAfter\(|clone|reset|applySiteConfig' \
  --type=php

Repository: yiipress/engine

Length of output: 22810


Make project processor registration idempotent per build.

The insertBefore() and insertAfter() calls mutate the injected singleton pipelines. While Symfony console typically creates fresh command instances per execution, the code violates idempotency principles. If a BuildCommand instance is reused (in tests, long-running harnesses, or custom service configurations), project processors accumulate and apply multiple times. Reset the pipelines before re-inserting, or construct fresh pipeline copies per execution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Console/BuildCommand.php` around lines 1518 - 1521, The
$this->contentPipeline and $this->feedPipeline are injected singleton objects
being mutated by insertBefore() and insertAfter() calls without reset, causing
processor accumulation if the BuildCommand instance is reused. Before the
insertBefore and insertAfter calls, either reset both pipelines by removing
existing processors or create fresh pipeline copies to ensure idempotency. This
prevents duplicate processor registration and execution in scenarios where the
BuildCommand is reused across multiple build executions.

Comment on lines +16 to +19
public function process(string $content, Entry $entry): string
{
return (string) ($this->callback)($content, $entry);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate callback return type or document the string cast behavior.

The string cast on line 18 may cause runtime errors or silent data corruption:

  • If the callback returns an object without __toString, PHP will throw a fatal error
  • If the callback returns null, it silently becomes an empty string
  • If the callback returns bool/int/float, values are silently converted

Consider adding explicit type validation with a helpful error message, or document that user callbacks must return string.

🛡️ Proposed fix to add validation
 public function process(string $content, Entry $entry): string
 {
-    return (string) ($this->callback)($content, $entry);
+    $result = ($this->callback)($content, $entry);
+    
+    if (!is_string($result)) {
+        throw new \RuntimeException(sprintf(
+            'Project processor callback must return string, %s returned',
+            get_debug_type($result)
+        ));
+    }
+    
+    return $result;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function process(string $content, Entry $entry): string
{
return (string) ($this->callback)($content, $entry);
}
public function process(string $content, Entry $entry): string
{
$result = ($this->callback)($content, $entry);
if (!is_string($result)) {
throw new \RuntimeException(sprintf(
'Project processor callback must return string, %s returned',
get_debug_type($result)
));
}
return $result;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Processor/CallbackContentProcessor.php` around lines 16 - 19, The process
method in the CallbackContentProcessor class performs an unsafe string cast on
the callback return value without validation. Add explicit type validation after
calling the callback to check if the return value is actually a string before
the cast. If the return value is not a string, throw an exception with a helpful
error message that indicates the callback must return a string type. This will
prevent silent data corruption from null values converting to empty strings,
boolean/integer/float conversions, or fatal errors from objects without
__toString methods.

@samdark samdark force-pushed the add-project-shortcodes branch from f0c5e96 to c4c2eec Compare June 15, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants