Skip to content

Chore: Type class constants and expand Pint rule set#250

Merged
loks0n merged 1 commit intomainfrom
chore/typed-consts-and-pint-rules
Apr 27, 2026
Merged

Chore: Type class constants and expand Pint rule set#250
loks0n merged 1 commit intomainfrom
chore/typed-consts-and-pint-rules

Conversation

@loks0n
Copy link
Copy Markdown
Contributor

@loks0n loks0n commented Apr 27, 2026

Summary

  • Add explicit types to all 110 class constants across the public API (string / int / array), now that the PHP floor is 8.3 and typed constants are supported.
  • Expand pint.json beyond the bare per preset with rules that mirror common editor inspections, so CI catches what the editor flags rather than leaving an editor-vs-CI gap.
  • Pint applied the new rules across the codebase. The largest visible change is \-prefixing compiler-optimized builtins (array_key_exists, count, strlen, is_array, in_array, func_get_args, etc.) in namespaced files — this lets the PHP opcode compiler emit specialized opcodes instead of going through namespace lookup. Non-optimized builtins are deliberately left unprefixed.

New Pint rules

  • native_function_invocation (@compiler_optimized, scope: namespaced, strict: true)
  • no_unused_imports, ordered_imports (alpha)
  • no_useless_else, no_useless_return
  • no_empty_statement, no_empty_phpdoc, no_empty_comment
  • array_syntax: short, single_quote
  • trailing_comma_in_multiline (arrays, arguments, parameters)

Test plan

  • composer format:check passes
  • composer analyze passes (PHPStan level 7)
  • composer refactor:check passes (Rector dry run clean)
  • CI: format / analyze / refactor / unit (8.3, 8.4, 8.5) / e2e (FPM, Swoole) green

Add explicit types to all 110 class constants across the public API
(string/int/array) — leveraging PHP 8.3 typed constants now that the
floor is 8.3.

Expand pint.json beyond the bare `per` preset with rules that match
common editor inspections, so CI catches what the editor flags:
- native_function_invocation (@compiler_optimized, namespaced scope)
- no_unused_imports, ordered_imports
- no_useless_else, no_useless_return
- no_empty_statement, no_empty_phpdoc, no_empty_comment
- array_syntax (short), single_quote
- trailing_comma_in_multiline

Pint applied the new rules across the codebase (mainly prefixing
compiler-optimized builtins with `\` in namespaced files and sorting
imports).
@github-actions
Copy link
Copy Markdown

k6 benchmark

Throughput Requests Fail rate p50 p95
8463 req/s 592523 0% 4.81 ms 10.4 ms

@loks0n loks0n merged commit bddf77e into main Apr 27, 2026
10 checks passed
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR types all 110 class constants across the public API (string / int / array) now that PHP 8.3 is the minimum, and expands the Pint rule set with native_function_invocation (@compiler_optimized), import ordering, and several code-quality rules. Pint was then applied uniformly: compiler-optimized builtins (strlen, count, in_array, array_key_exists, is_null, strval, etc.) are \-prefixed in namespaced files while non-optimized ones have their existing backslash prefix removed, and imports are alpha-sorted throughout.

Confidence Score: 4/5

Safe to merge; all changes are mechanical (tooling-driven) with no logic alterations, but typed public constants are a library API contract change.

No P0 or P1 issues. The single P2 concern is that adding explicit types to public/protected class constants is a semver-relevant change for library consumers who subclass these types — worth acknowledging in release notes even if intentional.

src/Http/Response.php, src/Http/Request.php, src/Http/Router.php, src/Http/View.php — these hold the public/protected typed constants that affect library consumers.

Important Files Changed

Filename Overview
pint.json Adds 10 new Pint rules on top of the per preset, including native_function_invocation for @compiler_optimized builtins in namespaced files; all rules are sound for PHP 8.3+.
src/Http/Response.php All 50+ status-code and content-type constants typed as int/string; \strlen, \is_array, \json_encode, etc. updated per @compiler_optimized rule. Typed public constants are a library API contract change.
src/Http/Request.php HTTP method constants typed as string; backslash prefix added to \count (compiler-optimized), removed from array_map, mb_strlen, implode (not compiler-optimized). Consistent with the new Pint rule.
src/Http/Http.php Request-method and mode-type constants typed as string/int; backslash handling updated consistently — \in_array, \call_user_func_array kept; parse_url, str_replace, str_contains un-prefixed.
src/Http/Adapter/Swoole/Server.php REQUEST_CONTAINER_CONTEXT_KEY typed as string; imports reordered alphabetically. No logic changes.
src/Http/Adapter/FPM/Request.php Backslash prefix removed from non-compiler-optimized functions (explode, trim, parse_url, substr, strpos); \strlen, \is_array retained correctly.
src/Http/Adapter/FPM/Response.php \is_null backslash added (compiler-optimized); header, setcookie un-prefixed (not compiler-optimized). Consistent with rule.
src/Http/Adapter/Swoole/Request.php \strlen, \strval prefixes added (compiler-optimized); parse_url un-prefixed. Consistent.
src/Http/Files.php EXTENSIONS constant typed as array; \array_key_exists, \in_array, \strlen, \strval backslash-prefixed correctly.
src/Http/Router.php PLACEHOLDER_TOKEN and WILDCARD_TOKEN typed as string; \array_key_exists, \in_array, \count consistently prefixed.
src/Http/View.php FILTER_ESCAPE and FILTER_NL2P typed as string; non-compiler-optimized calls un-prefixed correctly.
src/Http/Route.php \array_key_exists backslash added; \array_values un-prefixed. Consistent with rule.
tests/HttpTest.php Imports reordered alphabetically; ob_start/ob_get_contents/ob_end_clean un-prefixed (not compiler-optimized). No test logic changes.
tests/e2e/Client.php \strlen and \count correctly prefixed (compiler-optimized) inside a closure within a namespace.
tests/UtopiaFPMRequestTest.php \array_merge un-prefixed (not compiler-optimized). Single mechanical change.
example/src/server.php Import block reordered alphabetically. No logic changes.
rector.php Import reordered alphabetically (DisallowedEmptyRuleFixerRector moved); no logical changes to Rector configuration.
src/Http/Adapter/SwooleCoroutine/Server.php REQUEST_CONTAINER_CONTEXT_KEY typed as string; imports reordered. No logic changes.

Reviews (1): Last reviewed commit: "Chore: Type class constants and expand P..." | Re-trigger Greptile

Comment thread src/Http/Response.php
Comment on lines 11 to 117
/**
* HTTP content types
*/
public const CONTENT_TYPE_TEXT = 'text/plain';
public const string CONTENT_TYPE_TEXT = 'text/plain';

public const CONTENT_TYPE_HTML = 'text/html';
public const string CONTENT_TYPE_HTML = 'text/html';

public const CONTENT_TYPE_JSON = 'application/json';
public const string CONTENT_TYPE_JSON = 'application/json';

public const CONTENT_TYPE_XML = 'text/xml';
public const string CONTENT_TYPE_XML = 'text/xml';

public const CONTENT_TYPE_JAVASCRIPT = 'text/javascript';
public const string CONTENT_TYPE_JAVASCRIPT = 'text/javascript';

public const CONTENT_TYPE_IMAGE = 'image/*';
public const string CONTENT_TYPE_IMAGE = 'image/*';

public const CONTENT_TYPE_IMAGE_JPEG = 'image/jpeg';
public const string CONTENT_TYPE_IMAGE_JPEG = 'image/jpeg';

public const CONTENT_TYPE_IMAGE_PNG = 'image/png';
public const string CONTENT_TYPE_IMAGE_PNG = 'image/png';

public const CONTENT_TYPE_IMAGE_GIF = 'image/gif';
public const string CONTENT_TYPE_IMAGE_GIF = 'image/gif';

public const CONTENT_TYPE_IMAGE_SVG = 'image/svg+xml';
public const string CONTENT_TYPE_IMAGE_SVG = 'image/svg+xml';

public const CONTENT_TYPE_IMAGE_WEBP = 'image/webp';
public const string CONTENT_TYPE_IMAGE_WEBP = 'image/webp';

public const CONTENT_TYPE_IMAGE_ICON = 'image/x-icon';
public const string CONTENT_TYPE_IMAGE_ICON = 'image/x-icon';

public const CONTENT_TYPE_IMAGE_BMP = 'image/bmp';
public const string CONTENT_TYPE_IMAGE_BMP = 'image/bmp';

/**
* Chrsets
*/
public const CHARSET_UTF8 = 'UTF-8';
public const string CHARSET_UTF8 = 'UTF-8';

/**
* HTTP response status codes
*/
public const STATUS_CODE_CONTINUE = 100;
public const STATUS_CODE_SWITCHING_PROTOCOLS = 101;
public const STATUS_CODE_PROCESSING = 102;
public const STATUS_CODE_EARLY_HINTS = 103;

public const STATUS_CODE_OK = 200;
public const STATUS_CODE_CREATED = 201;
public const STATUS_CODE_ACCEPTED = 202;
public const STATUS_CODE_NON_AUTHORITATIVE_INFORMATION = 203;
public const STATUS_CODE_NOCONTENT = 204;
public const STATUS_CODE_RESETCONTENT = 205;
public const STATUS_CODE_PARTIALCONTENT = 206;
public const STATUS_CODE_MULTI_STATUS = 207;
public const STATUS_CODE_ALREADY_REPORTED = 208;
public const STATUS_CODE_IM_USED = 226;

public const STATUS_CODE_MULTIPLE_CHOICES = 300;
public const STATUS_CODE_MOVED_PERMANENTLY = 301;
public const STATUS_CODE_FOUND = 302;
public const STATUS_CODE_SEE_OTHER = 303;
public const STATUS_CODE_NOT_MODIFIED = 304;
public const STATUS_CODE_USE_PROXY = 305;
public const STATUS_CODE_UNUSED = 306;
public const STATUS_CODE_TEMPORARY_REDIRECT = 307;
public const STATUS_CODE_PERMANENT_REDIRECT = 308;

public const STATUS_CODE_BAD_REQUEST = 400;
public const STATUS_CODE_UNAUTHORIZED = 401;
public const STATUS_CODE_PAYMENT_REQUIRED = 402;
public const STATUS_CODE_FORBIDDEN = 403;
public const STATUS_CODE_NOT_FOUND = 404;
public const STATUS_CODE_METHOD_NOT_ALLOWED = 405;
public const STATUS_CODE_NOT_ACCEPTABLE = 406;
public const STATUS_CODE_PROXY_AUTHENTICATION_REQUIRED = 407;
public const STATUS_CODE_REQUEST_TIMEOUT = 408;
public const STATUS_CODE_CONFLICT = 409;
public const STATUS_CODE_GONE = 410;
public const STATUS_CODE_LENGTH_REQUIRED = 411;
public const STATUS_CODE_PRECONDITION_FAILED = 412;
public const STATUS_CODE_REQUEST_ENTITY_TOO_LARGE = 413;
public const STATUS_CODE_REQUEST_URI_TOO_LONG = 414;
public const STATUS_CODE_UNSUPPORTED_MEDIA_TYPE = 415;
public const STATUS_CODE_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
public const STATUS_CODE_EXPECTATION_FAILED = 417;
public const STATUS_CODE_IM_A_TEAPOT = 418;
public const STATUS_CODE_MISDIRECTED_REQUEST = 421;
public const STATUS_CODE_UNPROCESSABLE_ENTITY = 422;
public const STATUS_CODE_LOCKED = 423;
public const STATUS_CODE_FAILED_DEPENDENCY = 424;
public const STATUS_CODE_TOO_EARLY = 425;
public const STATUS_CODE_UPGRADE_REQUIRED = 426;
public const STATUS_CODE_PRECONDITION_REQUIRED = 428;
public const STATUS_CODE_TOO_MANY_REQUESTS = 429;
public const STATUS_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
public const STATUS_CODE_UNAVAILABLE_FOR_LEGAL_REASONS = 451;

public const STATUS_CODE_INTERNAL_SERVER_ERROR = 500;
public const STATUS_CODE_NOT_IMPLEMENTED = 501;
public const STATUS_CODE_BAD_GATEWAY = 502;
public const STATUS_CODE_SERVICE_UNAVAILABLE = 503;
public const STATUS_CODE_GATEWAY_TIMEOUT = 504;
public const STATUS_CODE_HTTP_VERSION_NOT_SUPPORTED = 505;
public const STATUS_CODE_VARIANT_ALSO_NEGOTIATES = 506;
public const STATUS_CODE_INSUFFICIENT_STORAGE = 507;
public const STATUS_CODE_LOOP_DETECTED = 508;
public const STATUS_CODE_NOT_EXTENDED = 510;
public const STATUS_CODE_NETWORK_AUTHENTICATION_REQUIRED = 511;
public const int STATUS_CODE_CONTINUE = 100;
public const int STATUS_CODE_SWITCHING_PROTOCOLS = 101;
public const int STATUS_CODE_PROCESSING = 102;
public const int STATUS_CODE_EARLY_HINTS = 103;

public const int STATUS_CODE_OK = 200;
public const int STATUS_CODE_CREATED = 201;
public const int STATUS_CODE_ACCEPTED = 202;
public const int STATUS_CODE_NON_AUTHORITATIVE_INFORMATION = 203;
public const int STATUS_CODE_NOCONTENT = 204;
public const int STATUS_CODE_RESETCONTENT = 205;
public const int STATUS_CODE_PARTIALCONTENT = 206;
public const int STATUS_CODE_MULTI_STATUS = 207;
public const int STATUS_CODE_ALREADY_REPORTED = 208;
public const int STATUS_CODE_IM_USED = 226;

public const int STATUS_CODE_MULTIPLE_CHOICES = 300;
public const int STATUS_CODE_MOVED_PERMANENTLY = 301;
public const int STATUS_CODE_FOUND = 302;
public const int STATUS_CODE_SEE_OTHER = 303;
public const int STATUS_CODE_NOT_MODIFIED = 304;
public const int STATUS_CODE_USE_PROXY = 305;
public const int STATUS_CODE_UNUSED = 306;
public const int STATUS_CODE_TEMPORARY_REDIRECT = 307;
public const int STATUS_CODE_PERMANENT_REDIRECT = 308;

public const int STATUS_CODE_BAD_REQUEST = 400;
public const int STATUS_CODE_UNAUTHORIZED = 401;
public const int STATUS_CODE_PAYMENT_REQUIRED = 402;
public const int STATUS_CODE_FORBIDDEN = 403;
public const int STATUS_CODE_NOT_FOUND = 404;
public const int STATUS_CODE_METHOD_NOT_ALLOWED = 405;
public const int STATUS_CODE_NOT_ACCEPTABLE = 406;
public const int STATUS_CODE_PROXY_AUTHENTICATION_REQUIRED = 407;
public const int STATUS_CODE_REQUEST_TIMEOUT = 408;
public const int STATUS_CODE_CONFLICT = 409;
public const int STATUS_CODE_GONE = 410;
public const int STATUS_CODE_LENGTH_REQUIRED = 411;
public const int STATUS_CODE_PRECONDITION_FAILED = 412;
public const int STATUS_CODE_REQUEST_ENTITY_TOO_LARGE = 413;
public const int STATUS_CODE_REQUEST_URI_TOO_LONG = 414;
public const int STATUS_CODE_UNSUPPORTED_MEDIA_TYPE = 415;
public const int STATUS_CODE_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
public const int STATUS_CODE_EXPECTATION_FAILED = 417;
public const int STATUS_CODE_IM_A_TEAPOT = 418;
public const int STATUS_CODE_MISDIRECTED_REQUEST = 421;
public const int STATUS_CODE_UNPROCESSABLE_ENTITY = 422;
public const int STATUS_CODE_LOCKED = 423;
public const int STATUS_CODE_FAILED_DEPENDENCY = 424;
public const int STATUS_CODE_TOO_EARLY = 425;
public const int STATUS_CODE_UPGRADE_REQUIRED = 426;
public const int STATUS_CODE_PRECONDITION_REQUIRED = 428;
public const int STATUS_CODE_TOO_MANY_REQUESTS = 429;
public const int STATUS_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
public const int STATUS_CODE_UNAVAILABLE_FOR_LEGAL_REASONS = 451;

public const int STATUS_CODE_INTERNAL_SERVER_ERROR = 500;
public const int STATUS_CODE_NOT_IMPLEMENTED = 501;
public const int STATUS_CODE_BAD_GATEWAY = 502;
public const int STATUS_CODE_SERVICE_UNAVAILABLE = 503;
public const int STATUS_CODE_GATEWAY_TIMEOUT = 504;
public const int STATUS_CODE_HTTP_VERSION_NOT_SUPPORTED = 505;
public const int STATUS_CODE_VARIANT_ALSO_NEGOTIATES = 506;
public const int STATUS_CODE_INSUFFICIENT_STORAGE = 507;
public const int STATUS_CODE_LOOP_DETECTED = 508;
public const int STATUS_CODE_NOT_EXTENDED = 510;
public const int STATUS_CODE_NETWORK_AUTHENTICATION_REQUIRED = 511;

/**
* @var array<int, string>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Typed constants are a breaking API change for library consumers

Adding explicit types to public and protected class constants (e.g., const int STATUS_CODE_OK, const string CONTENT_TYPE_JSON) is a PHP 8.3 feature. For a library like utopia-php/http, any downstream code that extends these classes and redeclares one of these constants with a different type — or that was relying on coercive assignment — will receive a fatal error at runtime in PHP 8.3+. This applies to the same constants typed across Request.php, Router.php, View.php, and the Swoole adapter files as well.

If this is intentional as part of a major/breaking release, consider documenting it in the changelog or PR description. If it isn't, this warrants a semver bump.

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.

1 participant