Provides classes for Complex numbers, Rational numbers, Vectors, and Matrices.
License | Changelog | Documentation
Work in progress. A native PHP extension that replicates the
OceanMoon PHP Math package - the Complex, Rational, Vector, and Matrix
classes - as a drop-in, faster substitute that also adds operators, something userland PHP classes can't
offer on their own.
Since each class uses the same fully-qualified name as its PHP package counterpart, loading this extension transparently replaces the userland class, with no code changes required. Without the extension loaded, the plain PHP package classes are used instead, so this is a purely additive, opt-in performance and ergonomics upgrade.
All four classes - Complex, Rational, Vector, and Matrix - are now fully implemented, including operator
overloading for each. This extension hasn't yet been tagged for release or published to
Packagist/PIE.
This package provides native C implementations of the Complex, Rational, Vector, and Matrix classes from
oceanmoon/math, for use as a faster, opt-in substitute for the userland versions.
Key Features:
- Drop-in replacement - each class uses the exact same fully-qualified name as its userland counterpart, so loading this extension transparently replaces it; no code changes required, and nothing breaks if the extension isn't loaded.
- Native performance -
Complex,Rational,Vector, andMatrixarithmetic implemented directly in C. - Operator overloading -
+,-,*,/,**(where applicable), and~(conjugate) forComplex;ComplexandRationalalso get the full comparison operator set (==,!=,<,<=,>,>=,<=>). Plain PHP classes can't do this on their own - it's only possible at the C level, via thedo_operation/compareobject handlers. - Behavioral parity - a PHPUnit conformance suite runs the userland package's own tests against this extension's native classes, so the two stay identical in behavior, not just API shape.
Claude Chat and Claude Code were used extensively in
the development of this extension - from designing the C-level object-handler architecture (do_operation/compare
overloads, lazily-computed properties, the custom clone_obj handler Matrix needs for deep-cloning its row Vectors)
to writing and reviewing the C implementation, stub files, and documentation. All code was reviewed by the author.
Correctness is validated by a PHPUnit conformance test suite that runs the userland package's own test suite
(oceanmoon/math) against this extension's native classes (tests/phpunit/), so behavior stays identical between the
two - not just method signatures - plus PHPStan (to level 9), including custom
operator-type-specifying extensions so static analysis understands the operator overloads that only exist here.
- PHP 8.4.x or 8.5.x (NTS or ZTS). The published build targets 8.4, matching the rest of the OceanMoon PHP package family; see "Why PHP 8.4?" below for how to build for 8.5 (or another version) yourself instead.
- To build from source: a C compiler and the PHP development headers (
phpize,php-config) - see Installation, below.
A compiled PHP extension is ABI-locked to the exact PHP minor version it was built against (its Zend Extension API
number) - the same .so can't just be loaded into a different version, and in the worst case a mismatched build
can compile "successfully" against the wrong version's headers while silently reading/writing the wrong struct layout at
runtime.
The published, prebuilt module is compiled against PHP 8.4, to match the minimum PHP version required by
oceanmoon/core, oceanmoon/math, and the other OceanMoon PHP packages. It's still ABI-locked to 8.4 specifically,
though - it won't load under 8.5 or any other version.
If you're on PHP 8.5, or want a build for some other reason, you can easily build it yourself, it's simple enough. The C source supports both 8.4 and 8.5 already, so this is just a case of building against the PHP version you have installed.
If you're on another version of PHP, in all likelihood the extension won't build due to changes in the internal Zend engine APIs and structures that PHP's C extension interface depends on. These aren't part of PHP's stable, backward-compatible userland API, and can change from one minor version to the next without notice. You can use #if PHP_VERSION_ID... macros to make version-specific changes (see complex.c for an example), or perhaps get an AI to help you, or just message me and I'll see what I can do.
See: Installation for using the extension in your project.
See: Development for building, testing, and the project's C source layout.
Each class's full API (properties, factory methods, conversion, comparison, everything else) is documented in the Math package itself - this extension is a drop-in replacement, not a different API. The pages below cover only what the extension adds: operator overloading.
Adds +, -, *, /, **, ~ (conjugate), and the full set of comparison operators (==, !=, <, <=, >,
>=, <=>, ordered lexicographically by real then imaginary part). See Complex operators.
Adds +, -, *, /, **, and the full set of comparison operators (==, !=, <, <=, >, >=, <=>). See
Rational operators.
Adds +, -, *, /. See Vector operators.
Adds +, -, *, /, **. See Matrix operators.
There are two groups of comparison operators in PHP:
- Loose:
<=>,==,!=,<,<=,>,>=. Flexible about type. - Strict:
===,!==. Include type in the comparison, and for objects mean reference identity rather than value equality.
A PHP extension can't override a subset of the loose group independently: a single compare object handler backs <=>,
and PHP derives the other five (==, !=, <, <=, >, >=) from its result - there's no way to implement some of
the six and fall back to PHP's default for the rest.
Complex and Rational each provide one; Vector and Matrix don't, since there's no natural way to order a whole
element list against another the way there is for a 2-element (real, imaginary) tuple or a single rational value.
Without an extension-provided handler, Vector/Matrix still get PHP's own default object compare handler (like
any plain PHP object) - see the Math package's
Comparison Operators doc.
Rationalhas a genuine natural ordering, so its comparison operators mean exactly what you'd expect - see Rational operators.Complex's ordering is plain lexicographic (real part first, then imaginary) - useful for sorting and deduplication, but not mathematically meaningful, since there's no total order compatible with complex arithmetic. It's exactly what PHP's own default object comparison already gives twoComplexinstances for free ($realis declared before$imaginary); the operators only add accepting anint/floatoperand on either side. See Complex operators for the details.
Both accept an int/float operand on either side, promoted the same way their equal() method promotes one, and
throw for a NAN operand (no meaningful comparison result) - see each class's own docs for specifics.
=== and !== can't be overridden by a PHP extension, so they behave as normal. For objects, they always mean
reference identity: two distinct Complex/Rational/Vector/Matrix instances representing the same value are never
===, even when they are == or equal():
$z1 = new Complex(3, 4);
$z2 = new Complex(3, 4);
$z1 == $z2; // true (same value)
$z1 === $z2; // false (different instances)Vector and Matrix do still have comparison operators - PHP's default per-property comparison, same as any plain
PHP object, not an extension-provided handler (see Loose comparison operators above).
For ==/!= this happens to give a reasonable element-wise equality result, but without approxEqual()'s
floating-point tolerance; the ordering operators (<, <=, >, >=, <=>) aren't mathematically meaningful, for
the same reason Complex's aren't.
Some coding standards (PHPStan strict rules, Slevomat, and others) discourage ==/!= in favour of explicit method
calls regardless, so equal()/approxEqual() remain the recommended way to test value equality across all four
classes. All four are documented in the Math package documentation, which applies equally to the extension.
See:
Complex::equal()Complex::approxEqual()Rational::equal()Rational::approxEqual()Vector::equal()Vector::approxEqual()Matrix::equal()Matrix::approxEqual()
A fluent chain of method calls has no notion of precedence at all - it only ever evaluates in the order you nest the
calls, so $z1->add($z2->mul($z3)) requires you to have already worked out the correct grouping yourself before writing
a single method call. Operators come with precedence and associativity rules that PHP itself resolves instead, so
$z1 + $z2 * $z3 reads the same as ordinary arithmetic and PHP evaluates $z2 * $z3 first automatically.
That precedence isn't something any of these classes control, though. A PHP extension can overload what an operator
does (via the do_operation/compare object handlers, which is how every operator on this page exists at all) but
not how tightly it binds relative to other operators - precedence and associativity are fixed by the language grammar
itself, entirely outside any extension's control. So none of Complex/Rational/Vector/Matrix get their own
precedence rules; they all just inherit PHP's, same as int/float.
This is actually a blessing - most PHP developers will know the most important operator precedence rules already (multiplication before addition, etc.) - so, keeping them fixed should reduce bugs. Refer to the full PHP operator precedence table to revise.
The table below shows just the rules relevant to this extension, tightest-binding first:
| Precedence | Operators | Associativity | Used by | Notes |
|---|---|---|---|---|
| 1 (highest) | ** |
Right | Complex, Rational, Matrix |
Binds tighter than unary -/+/~ on its left operand - see below. |
| 2 | - + ~ (unary prefix) |
Right | - + All four; ~ is Complex-only |
-$z ** 2 is -($z ** 2), not (-$z) ** 2 - PHP's well-known ** special case, not specific to this extension. |
| 3 | * / |
Left | All four | |
| 4 | - + (binary) |
Left | All four | |
| 5 | < <= > >= |
Non-assoc | Complex, Rational |
Looser than arithmetic, so $r1 + $r2 < $r3 is ($r1 + $r2) < $r3. |
| 6 (lowest) | == != <=> |
Non-assoc | Complex, Rational |
Looser again than </<=/>/>= - $r1 <=> $r2 == 1 is a parse error, matching PHP's own non-associativity here. |
The most tricky thing to remember is that the exponentiation operator ** has the highest precedence of all, which
means, for example, -$z ** 2 is evaluated as -($z ** 2) rather than (-$z) ** 2. That precedence can be
unobvious because typical code formatting omits spaces around unary operators, but includes them around binary ones,
suggesting unary always trumps binary. This is usually true, but not in the case of **.
Examples:
$z1 = new Complex(1, 1);
$z2 = new Complex(2, 0);
$z3 = new Complex(3, 0);
$z1 + $z2 * $z3; // $z2 * $z3 is evaluated first: $z1 + ($z2 * $z3)
-$z1 ** 2; // ** is evaluated first: -($z1 ** 2) = -2i
(-$z1) ** 2; // parentheses override precedence: 2i (differs from the line above)The main downside to using the extension is poor support for custom operators in IDEs like PhpStorm, static analysis tools like PHPStan, and diagnostic tools like Intelephense.
See Static Analysis for possible solutions and workarounds (work in progress).
MIT License - see LICENSE for details
- Issues: https://github.com/mossy2100/PHP-Math-extension/issues
- Documentation: See docs/ directory for detailed class documentation
- Examples: See test files for comprehensive usage examples
For questions or suggestions, please open an issue.
See CHANGELOG.md for version history and changes.
