https://a.storyblok.com/f/270183/1368x665/a914c53d00/26mar_dev-blog_read-php-code.jpg

If You Can’t Read Your PHP Code, Neither Can Your LLM

Published on April 2, 2026

Time to read: 6 minutes

There are many misconceptions, opinions, and influencer spiel around AI. Everyone is asking, “Are you AI-ready?”, a very valid question.

AI won’t do everything for you. It’ll have a go at doing everything for you if you let it, but depending on the stack you are using, the results will probably be a mixed bag. The most important way to treat AI is as an extra set of tools in your arsenal. This, as a result, is just another iteration of having a decent set of tools such as static analysis, code linters, and well-written tests.

To prepare your codebase for AI usage, we’re going to dip into how to use PHP’s Readalizer, because AI tooling is only as good as the code that it’s reading.

What Is Readalizer?

Readalizer is a PHP library that gives developers an API comprised of a set of rules that it will enforce in your codebase. At runtime, it ingests each PHP file defined in its configuration, and tokenises your code using the Abstract Syntax Tree. An example of a rule would be NoTodoWithoutTicketRule. At runtime, your code:

public function(stdObj $myExample) {
	// TODO add some logic
	return stdObject->valid === true; 
}

This will fail. The rule states that some sort of ticket reference must be provided within the inline comment. So, to fix it, we need to modify our comment:

public function(stdObj $myExample) {
	// TODO add some logic (DEVEX-2541)
	return stdObject->valid === true; 
}

There are a lot of rules, so we can’t go through them all. What we will do, though, is show an implementation and why that matters on a live codebase.

Wait, How Does This Differ From Static Analysis?

Ah, I’m glad you asked that. Because, essentially, a set of rules and a pass-or-fail tool looks similar to static analysers such as PHPStan or PsalmPHP (both also use the AST to parse PHP code into tokens). There is a very distinct difference between the two, though. Static analysis ensures that your code works. Readalizer doesn’t care if your code runs or not, just that it follows the rulesets you’ve given it. Therefore, we can conclude that a static analyzer is part of your code quality toolchain, but Readalizer’s biggest use case is to make sure that both humans and Large Language Models can read your code and make improved decisions due to the rulesets you’ve implemented.

Readalizer in the Author’s Own Words

Something I quite like to do in articles now is ask the creators of a project how it came about and the project’s goal. So, here are some words about what Readalizer is, in Christopher Miller’s words:

Readalizer was designed from the ground up to support both you and your AI Agents to understand your PHP Code. The inspiration for even starting this package was a quote from Ondrej Mirtes, who said that PHPStan isn't concerned with what your code looks like; that's someone else's job. So, I made it mine. Having spent most of my career talking about code that is readable, I thought it was about time I did something about it. That’s where Readalizer comes in. It allows you to customise the rules with configurations, custom rules and custom rulesets, while getting started in around 30 seconds."

Running Readalizer on the Vonage PHP SDK

To see how this works in practice, let’s run Readalizer against a real-world codebase: the Vonage PHP SDK.

The goal here is simple: to identify where improving code consistency can make the project easier for both humans and LLMs to understand.

Getting started takes just two steps:

composer require readalizer/readalizer

vendor/bin/readalizer --init

This will give you a readalizer.php config in the root of your project. Excellent. I modified the boilerplate to instruct it to look in the src directory by default, and temporarily disabled all rules so we can enable them one at a time and see their impact.

<?php

declare(strict_types=1);

use Readalizer\Readalizer\Rules\ClassNamePascalCaseRule;

use Readalizer\Readalizer\Rules\NoStaticPropertyRule;

use Readalizer\Readalizer\Rules\RequireNamespaceRule;

use Readalizer\Readalizer\Rules\StrictTypesDeclarationRule;

use Readalizer\Readalizer\Rules\PropertyNameCamelCaseRule;

use Readalizer\Readalizer\Rules\ConstantUpperCaseRule;

use Readalizer\Readalizer\Rules\ExceptionSuffixRule;

/**

* Copy this to readalizer.php in your project root and configure your rules.

*

* Each rule is a class implementing RuleInterface (node-level) or

* FileRuleInterface (file-level). Rules can live anywhere.

*

* ── Suppressing violations ───────────────────────────────────────────────────

*

* PHP attribute on a class, method, property, or parameter:

*

*   use Readalizer\Readalizer\Attributes\Suppress;

*

*   #[Suppress]                                   // suppress ALL rules

*   #[Suppress(NoLongMethodsRule::class)]          // suppress one rule

*   #[Suppress(RuleA::class, RuleB::class)]        // suppress multiple

*

* Scope: a class-level attribute suppresses everything within the class;

* a method-level attribute suppresses everything within that method.

*

* Inline comment for line-level suppression (trailing or preceding line):

*

*   $x = something(); // @readalizer-suppress NoLongMethodsRule

*   // @readalizer-suppress                   (preceding line, suppress all)

*   // @readalizer-suppress RuleA, RuleB      (preceding line, suppress named)

*/

return [

   // Paths to scan when no paths are passed on the CLI.

   'paths' => [

       'src',

   ],

   // Memory limit for analysis (default: 2G).

   'memory_limit' => '2G',

   // Cache results between runs.

   'cache' => [

       'enabled' => true,

       'path' => '.readalizer-cache.json',

   ],

   // Optional baseline file to suppress known violations.

   // 'baseline' => '.readalizer-baseline.json',

   // Paths, directory prefixes, or glob patterns to exclude from scanning.

   'ignore' => [

       // 'rector.php',

       // 'src/',

       // '*.generated.php',

   ],

   // Choose one or more rulesets (packs).

   'ruleset' => [

       // new FileStructureRuleset(),

       // new TypeSafetyRuleset(),

       // new ClassDesignRuleset(),

       // new MethodDesignRuleset(),

       // new NamingRuleset(),

       // new ExpressionRuleset(),

   ],

   // Add or override rules on top of rulesets.

   'rules' => [

       // File structure

       // new LineLengthRule(maxLeng)th: 120),

       // new CustomFileRule(),

       // Type safety

       // new CustomTypeRule(),

       // Class design

       // new CustomClassRule(),

       // Method design

       // new CustomMethodRule(),

       // Naming conventions

       // new CustomNamingRule(),

       // Expressions & control flow

       // new CustomExpressionRule(),
   ],
];

. Next, I had a look at adding rules, one by one. First up, I added the ClassNamePascalCaseRule(), which makes sure that every class in your source code is in PascalCase (so, WordPress developers might want to look away). I ran Readalizer and got the results:

[##############################] 100% (319/319) 1s

[OK] No readability violations found.

Time: 1.06s

No violations here. Good work, SDK authors! I guess I’d better pick another one. So, I went with the ExceptionSuffixRule(). This rule says that all of your /Exception’s must be identifiable by having Exception in the class name. For an LLM, that is important because file structure and naming conventions give it predictability. What happens when we run this, then?

[##############################] 100% (319/319) 1s

src/Client/Exception/Conflict.php

  line [ExceptionSuffixRule]  Exception class "Conflict" should end with "Exception".

src/Client/Exception/Credentials.php

  line [ExceptionSuffixRule]  Exception class "Credentials" should end with "Exception".

src/Client/Exception/NotFound.php

  line [ExceptionSuffixRule]  Exception class "NotFound" should end with "Exception".

src/Client/Exception/Request.php

  line 10  [ExceptionSuffixRule]  Exception class "Request" should end with "Exception".

src/Client/Exception/Server.php

  line 10  [ExceptionSuffixRule]  Exception class "Server" should end with "Exception".

src/Client/Exception/Transport.php

  line [ExceptionSuffixRule]  Exception class "Transport" should end with "Exception".

[FAIL] Found 6 violations.

Time: 1.10s

Aha! Now we’re getting somewhere. It’s not a bad decision to have exceptions written like this - they are, after all, defined within namespace structure as Exceptions. But to reiterate: you want your LLM to have as little work to do as possible to make the next decision. Again, predictability in your codebase is the key here.

With only six violations, and the fix being to rename six classes, this seems like a pretty trivial job for Claude to fix. So, I open up my prompt in Cursor, paste in the violations, and tell it to refactor them. I push the code up, create a PR, and hey presto:

https://github.com/Vonage/vonage-php-sdk-core/pull/553

It seems like a small example, but just to sanity check, I even asked Glean to tell me in its own words what improvements I can make to an SDK to make the agent experience better. Its output confirmed what I assumed to be the case:

  • Add structured reasoning fields to your API responses

  • Use consistent naming conventions - LLMs need predictability to make decisions

  • Include resolution actions in error responses so agents know how to fix problems

// Example: Exception naming for predictability

// BAD - LLM can't predict this is an exception

class Invalid {}

// GOOD - Clear suffix helps LLM identify exceptions

class InvalidRequestException {}

There you have it, from the agent itself with a one-sentence prompt.

Conclusion

While a simple example here serves to give a very gentle outline on how to be AI-ready, it’s also worth mentioning that the agent experience is a multi-faceted approach. To succeed with AI, I wouldn’t listen, for example, to the many, many influencers on LinkedIn feeds stating “X is bad, use Y”. That’s too simplistic. Vonage’s approach is much like other industry leaders in that it’s a cross-platform effort:

  • This article outlined tools to improve your SDKs for Agents running just as an LLM, such as OpenClaw

  • On top of this, an Agent should be able to have the option to use an MCP server as an alternative option (Vonage, for instance have released our own), which in turn should be hooked into:

  • A refactored and hardened Command Line Interface (CLI, and yep, Vonage has had one for a long time) codebase and public-facing command structure so that Agents have the option to just hit the CLI on its own.

Three approaches, allowing AI tooling to decide what best accomplishes the task it has been given. That’s how you make yourself AI-ready.

Have a question or want to share what you're building?

Stay connected and keep up with the latest developer news, tips, and events.

Share:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeSenior PHP Developer Advocate

A trained actor with a dissertation on standup comedy, I came into PHP development via the meetup scene. You can find me speaking and writing on tech, or playing/buying odd records from my vinyl collection.