Mutable class representing a two-dimensional matrix of numbers with comprehensive linear algebra operations.
The Matrix class provides a complete implementation of matrix arithmetic with support for:
- Basic arithmetic (addition, subtraction, scalar/matrix multiplication, division, Hadamard product)
- Matrix-vector multiplication using column vector convention
- Matrix power with binary exponentiation (including negative powers via inverse)
- Transpose, determinant, and inverse operations
- Sub-matrix extraction, in-place pasting, and resizing
- Element and row/column access with bounds checking
- String representation using box-drawing characters
- Support for n⨉0 and 0⨉n empty matrices
Matrix data is stored privately to prevent non-rectangular or non-numeric mutation.
private(set) int $rowCountThe number of rows in the matrix. Read-only from outside the class. Stored explicitly (not derived from the data array) to support 0-column matrices where the row count cannot be inferred from data.
private(set) int $columnCountThe number of columns in the matrix. Read-only from outside the class. Stored explicitly for the same reason as
rowCount, to support 0-row matrices.
public function __construct(int $rowCount, int $columnCount)Create a new zero-filled matrix with the specified dimensions.
Parameters:
$rowCount(int) - Number of rows (must be non-negative)$columnCount(int) - Number of columns (must be non-negative)
Throws: DomainException if either dimension is negative.
Examples:
$m1 = new Matrix(3, 3); // 3x3 zero matrix
$m2 = new Matrix(2, 4); // 2x4 zero matrix
$m3 = new Matrix(0, 0); // 0x0 empty matrix
$m4 = new Matrix(0, 3); // 0x3 empty matrixpublic static function fromArray(array $arr): selfCreate a matrix from a 2D array. The outer array and every row must be a list (sequential integer keys starting at 0); a non-sequential array is rejected rather than silently re-indexed. All rows must have the same number of elements, and every element must be numeric. Integer values are cast to float.
Parameters:
$arr(array<array-key, mixed>) - Rectangular list of rows of numbers.
Returns: self - New matrix populated with the provided data.
Throws:
DomainExceptionif the outer array or any row is not a list, or any element is not a number.LengthExceptionif the rows don't all have the same number of columns.
Examples:
$m = Matrix::fromArray([
[1, 2, 3],
[4, 5, 6],
]);
// 2x3 matrix
$m0 = Matrix::fromArray([]);
// 0x0 matrixpublic static function identity(int $size): selfCreate an identity matrix of the specified size. The identity matrix has 1s on the main diagonal and 0s elsewhere.
Parameters:
$size(int) - Size of the identity matrix (both rows and columns)
Returns: self - Identity matrix.
Throws: DomainException if $size is negative.
Examples:
$idMat = Matrix::identity(3);
// ┌ ┐
// │ 1.0 0.0 0.0 │
// │ 0.0 1.0 0.0 │
// │ 0.0 0.0 1.0 │
// └ ┘public function toArray(): arrayGet a copy of the matrix data as a rectangular array.
Returns: list<list<float>> - Rectangular array of matrix elements.
Examples:
$m = Matrix::fromArray([[1, 2], [3, 4]]);
$arr = $m->toArray(); // [[1, 2], [3, 4]]public function __toString(): stringConvert the matrix to a string representation using box-drawing characters. Values are right-aligned within columns.
Examples:
$m = Matrix::fromArray([[1, 2], [3, 4]]);
echo $m;
// ┌ ┐
// │ 1.0 2.0 │
// │ 3.0 4.0 │
// └ ┘
// Empty matrices
$m0 = new Matrix(0, 0);
echo $m0;
// ┌ ┐
// └ ┘public function isSquare(?int $size = null): boolCheck if the matrix is square, optionally of a specific size.
Parameters:
$size(int|null) - If specified, check for exact size; otherwise any square matrix returns true.
Returns: bool - True if the matrix is square (and of the specified size, if given).
Examples:
$idMat = Matrix::identity(3);
var_dump($idMat->isSquare()); // true
var_dump($idMat->isSquare(3)); // true
var_dump($idMat->isSquare(2)); // false
$m1 = new Matrix(2, 3);
var_dump($m1->isSquare()); // falsepublic function get(int $row, int $col): floatGet a matrix element by row and column index.
Parameters:
$row(int) - Row index (0-based).$col(int) - Column index (0-based).
Returns: float - The value at the specified position.
Throws: OutOfRangeException if either index is outside the valid range.
Examples:
$m = Matrix::fromArray([[1, 2], [3, 4]]);
echo $m->get(0, 0); // 1
echo $m->get(1, 1); // 4public function getRow(int $row): VectorGet a row as a Vector.
Returns an independent copy - mutating the returned Vector never affects the Matrix. For a live, mutable view of a
row that stays linked to the Matrix, use $m[$row] instead.
Parameters:
$row(int) - Row index (0-based)
Returns: Vector - An independent copy of the row.
Throws: OutOfRangeException if row index is outside the valid range.
Examples:
$m = Matrix::fromArray([[1, 2, 3], [4, 5, 6]]);
$row = $m->getRow(0); // Vector(1, 2, 3)public function getColumn(int $col): VectorGet a column as a Vector.
Parameters:
$col(int) - Column index (0-based)
Returns: Vector - The column as a Vector.
Throws: OutOfRangeException if column index is outside the valid range.
Examples:
$m = Matrix::fromArray([[1, 2], [3, 4], [5, 6]]);
$col = $m->getColumn(0); // Vector(1, 3, 5)public function copy(int $row, int $col, int $rowCount, int $colCount): selfExtract a rectangular sub-matrix: a copy of a subset of the matrix's elements. Purely functional — does not modify the matrix.
Parameters:
$row(int) - Row of the top-left corner of the region to copy (0-based).$col(int) - Column of the top-left corner of the region to copy (0-based).$rowCount(int) - Number of rows to copy.$colCount(int) - Number of columns to copy.
Returns: self - A new matrix containing the copied elements.
Throws: OutOfRangeException if either count is negative, or the selected region extends beyond the matrix's
bounds.
Examples:
$m = Matrix::fromArray([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]);
$sub = $m->copy(1, 1, 2, 2);
// [[5, 6],
// [8, 9]]public function set(int $row, int $col, float $value): voidSet a matrix element by row and column index.
Parameters:
$row(int) - Row index (0-based)$col(int) - Column index (0-based)$value(float) - Value to set
Throws:
OutOfRangeExceptionif either index is outside the valid range.DomainExceptionif the value is not finite (±INF or NAN).
Examples:
$m = new Matrix(2, 2);
$m->set(0, 0, 5);
$m->set(1, 1, 10);public function setRow(int $row, Vector $vec): voidSet a row from a Vector. $vec's elements are copied into the row's existing Vector, which is never replaced - the
row's object identity is preserved, so a live reference obtained via $m[$row] stays valid and
reflects the new values. $vec itself is never stored by reference: mutating it afterward has no effect on the Matrix.
Parameters:
$row(int) - Row index (0-based)$vec(Vector) - The row Vector
Throws:
OutOfRangeExceptionif row index is outside the valid range.LengthExceptionif the Vector has the wrong number of elements.
Examples:
$m = Matrix::fromArray([[1, 2, 3], [4, 5, 6]]);
$m->setRow(1, Vector::fromArray([10, 11, 12]));public function setColumn(int $col, Vector $vec): voidSet a column from a Vector.
Parameters:
$col(int) - Column index (0-based)$vec(Vector) - The column Vector
Throws:
OutOfRangeExceptionif column index is outside the valid range.LengthExceptionif the Vector has the wrong number of elements.
Examples:
$m = Matrix::fromArray([[1, 2, 3], [4, 5, 6]]);
$m->setColumn(1, Vector::fromArray([20, 50]));
// Matrix is now [[1, 20, 3], [4, 50, 6]]public function paste(self $other, int $row = 0, int $col = 0): voidCopy the elements of another matrix into the matrix, starting at the given position. Unlike most methods in this class,
paste() mutates in place, matching set(), setRow(), and setColumn().
Parameters:
$other(self) - The matrix to paste. Must fit within the matrix at the given offset.$row(int) - Row at which to place the top-left corner of$other(0-based). Defaults to 0.$col(int) - Column at which to place the top-left corner of$other(0-based). Defaults to 0.
Throws: OutOfRangeException if either offset is negative, or $other doesn't fit within the matrix at that
offset.
Examples:
$m = new Matrix(3, 3);
$m->paste(Matrix::fromArray([[1, 2], [3, 4]]), 1, 1);
// [[0, 0, 0],
// [0, 1, 2],
// [0, 3, 4]]The equal() and approxEqual() methods are provided by the
ApproxEquatable trait
from the Core package, with Matrix supplying its own type-checking logic
since the trait's parameter is typed mixed (see the trait's docs for why).
Both methods accept only a Matrix for $other — not a Vector or a plain array, even though those could plausibly
represent the same values. Anything else throws InvalidArgumentException rather than silently returning false, to
catch bugs from comparing values that can't meaningfully be compared.
public function equal(mixed $other): boolCheck if the matrix exactly equals another value.
Two matrices are equal if they have the same dimensions and all corresponding elements are exactly equal.
Parameters:
$other(mixed) - The value to compare with (must be aMatrix).
Returns: bool - True if the matrices have the same dimensions and all elements are exactly equal.
Throws: InvalidArgumentException if $other is not a Matrix.
Examples:
$m1 = Matrix::fromArray([[1, 2], [3, 4]]);
$m2 = Matrix::fromArray([[1, 2], [3, 4]]);
$m3 = Matrix::fromArray([[1.0000000001, 2], [3, 4]]);
var_dump($m1->equal($m2)); // true (exact match)
var_dump($m1->equal($m3)); // false (not exact)
// Anything else throws, rather than silently returning false
$m1->equal([[1, 2], [3, 4]]); // throws InvalidArgumentException
$m1->equal('string'); // throws InvalidArgumentException
$m1->equal(null); // throws InvalidArgumentExceptionpublic function approxEqual(
mixed $other,
float $relTol = Floats::DEFAULT_RELATIVE_TOLERANCE,
float $absTol = Floats::DEFAULT_ABSOLUTE_TOLERANCE
): boolCheck if the matrix approximately equals another value within specified tolerances.
Each pair of corresponding elements is compared using Floats::approxEqual(), which checks absolute tolerance first,
then relative tolerance.
Parameters:
$other(mixed) - The value to compare with (must be aMatrix).$relTol(float) - Relative tolerance (default: 1e-9).$absTol(float) - Absolute tolerance (default: PHP_FLOAT_EPSILON).
Returns: bool - True if the matrices have the same dimensions and all elements are approximately equal.
Throws:
InvalidArgumentExceptionif$otheris not aMatrix.DomainExceptionif either tolerance is negative.
@see Floats::approxEqual()
Examples:
$m1 = Matrix::fromArray([[1, 2], [3, 4]]);
$m2 = Matrix::fromArray([[1.00000001, 2.00000001], [3.00000001, 4.00000001]]);
// Within default tolerance
var_dump($m1->approxEqual($m2)); // true
// With tight tolerance
var_dump($m1->approxEqual($m2, 1e-15, 1e-15)); // false
// Anything else throws, rather than silently returning false
$m1->approxEqual('string'); // throws InvalidArgumentExceptionSee: Comparison Operators to read more about how the ==, <, etc. operators work for the
types in this package.
public function resize(int $rowCount, int $columnCount): selfCreate a new matrix with the given dimensions, containing as much of the matrix's data as fits. The result is anchored
at (0, 0): if the new dimensions are larger than the matrix's, the extra rows and/or columns are zero-filled; if
smaller, the excess rows/columns (from the bottom and/or right) are dropped. To resize from a different corner, or to
insert/remove a row or column at an arbitrary position, compose copy() and paste() directly instead.
Parameters:
$rowCount(int) - The number of rows in the resized matrix.$columnCount(int) - The number of columns in the resized matrix.
Returns: self - A new matrix with the given dimensions.
Throws: DomainException if either dimension is negative.
Examples:
$m = Matrix::fromArray([[1, 2], [3, 4]]);
// Grow: extra rows/columns are zero-filled
$grown = $m->resize(3, 3);
// [[1, 2, 0],
// [3, 4, 0],
// [0, 0, 0]]
// Shrink: excess rows/columns are dropped
$shrunk = $m->resize(1, 1);
// [[1]]public function neg(): selfNegate the matrix. Returns a new matrix with all elements negated.
Returns: self - A new matrix with all elements negated.
Example:
$m = Matrix::fromArray([[1, -2], [3, -4]]);
$result = $m->neg();
// [[-1, 2], [-3, 4]]public function reciprocal(): selfCalculate the element-wise reciprocal of the matrix. Not to be confused with inv(), the matrix inverse - reciprocal()
simply replaces each element with its own reciprocal, with no relationship to matrix multiplication.
Returns: self - A new matrix with each element replaced by its reciprocal.
Throws: ArithmeticException if any element is zero.
Example:
$m = Matrix::fromArray([[2, 4], [5, 10]]);
$result = $m->reciprocal();
// [[0.5, 0.25], [0.2, 0.1]]public function inv(): selfCalculate the inverse of the matrix. Uses cofactor expansion with the adjugate matrix. The matrix must be square and invertible (non-zero determinant).
Note: The underlying algorithm has O(n! × n²) time complexity. It is suitable for small matrices (up to ~10×10) but will be extremely slow for larger ones.
Returns: self - New matrix representing the inverse.
Throws:
DomainExceptionif the matrix is not square.ArithmeticExceptionif the matrix is not invertible (determinant is zero).
Examples:
$m = Matrix::fromArray([[1, 2], [3, 4]]);
$inv = $m->inv();
// Verify: M × M⁻¹ = I
$idMat = $m->mul($inv);public function add(self $other): selfAdd one matrix to another. Both matrices must have the same dimensions.
Parameters:
$other(self) - Matrix to add
Returns: self - New matrix representing the sum.
Throws: LengthException if matrices have different dimensions.
Examples:
$m1 = Matrix::fromArray([[1, 2], [3, 4]]);
$m2 = Matrix::fromArray([[5, 6], [7, 8]]);
$sum = $m1->add($m2);
// [[6, 8], [10, 12]]public function sub(self $other): selfSubtract one matrix from another. Both matrices must have the same dimensions.
Parameters:
$other(self) - Matrix to subtract
Returns: self - New matrix representing the difference.
Throws: LengthException if matrices have different dimensions.
Examples:
$m1 = Matrix::fromArray([[5, 6], [7, 8]]);
$m2 = Matrix::fromArray([[1, 2], [3, 4]]);
$diff = $m1->sub($m2);
// [[4, 4], [4, 4]]public function mul(float|self $other): selfMultiply the matrix by a scalar or another matrix.
When multiplying by a scalar, each element is scaled. When multiplying by a matrix, standard matrix multiplication is performed (the number of columns in the matrix must equal the number of rows in the other).
To multiply by a Vector (Ax), call mulVector() instead.
To go the other way (xA), call Vector::mul().
Parameters:
$other(float|self) - Number or matrix to multiply by
Returns: self - A new matrix representing the product.
Throws: LengthException if dimensions are incompatible for matrix multiplication.
Examples:
$m1 = Matrix::fromArray([[1, 2], [3, 4]]);
// Scalar multiplication
$scaled = $m1->mul(2);
// [[2, 4], [6, 8]]
// Matrix multiplication
$m2 = Matrix::fromArray([[5, 6], [7, 8]]);
$product = $m1->mul($m2);
// [[19, 22], [43, 50]]public function div(float $scalar): selfDivide the matrix by a scalar.
Parameters:
$scalar(float) - Number to divide by.
Returns: self - New matrix representing the quotient.
Throws: ArithmeticException if $scalar is zero.
Examples:
$m = Matrix::fromArray([[2, 4], [6, 8]]);
$result = $m->div(2);
// [[1, 2], [3, 4]]public function hadamardMul(self $other): selfCalculate the Hadamard product (element-wise product) of one matrix with another. Both matrices must have the same dimensions.
Parameters:
$other(self) - Matrix to multiply element-wise with.
Returns: self - New matrix representing the Hadamard product.
Throws: LengthException if matrices have different dimensions.
Examples:
$m1 = Matrix::fromArray([[1, 2], [3, 4]]);
$m2 = Matrix::fromArray([[5, 6], [7, 8]]);
$result = $m1->hadamardMul($m2);
// [[5, 12], [21, 32]]public function hadamardDiv(self $other): selfCalculate the Hadamard division (element-wise quotient) of one matrix by another. Both matrices must have the same dimensions.
Parameters:
$other(self) - Matrix to divide element-wise by.
Returns: self - New matrix representing the Hadamard quotient.
Throws:
LengthExceptionif matrices have different dimensions.ArithmeticExceptionif any element of$otheris zero.
Examples:
$m1 = Matrix::fromArray([[5, 12], [21, 32]]);
$m2 = Matrix::fromArray([[5, 6], [7, 8]]);
$result = $m1->hadamardDiv($m2);
// [[1, 2], [3, 4]]public function pow(int $exp): selfRaise the matrix to an integer power. Uses exponentiation by squaring for efficiency. The matrix must be square.
For a negative exponent, the base is inverted before squaring rather than squaring the base and inverting the result
at the end, so intermediate values shrink toward the (typically small) final answer instead of risking an unnecessary
overflow to INF on the way there. This also avoids any overflow hazard at PHP_INT_MIN, the most extreme negative
exponent.
Special cases:
- A^0 = identity matrix (for any square A)
- A^1 = A (equivalent to
clone; returns a new, distinct object that's equal to but not the same instance as$this) - A^(negative) = inverse of A raised to the corresponding positive power
Parameters:
$exp(int) - Power to raise to
Returns: self - New matrix representing the result.
Throws:
DomainExceptionif the matrix is not square.ArithmeticExceptionif the matrix is not invertible (zero determinant) for negative powers.
Examples:
$m1 = Matrix::fromArray([[1, 1], [0, 1]]);
$m2 = $m1->pow(0); // Identity matrix
$m3 = $m1->pow(2); // [[1, 2], [0, 1]]
$m4 = $m1->pow(3); // [[1, 3], [0, 1]]
$m5 = $m1->pow(-1); // Inverse matrixpublic function sqr(): selfSquare the matrix. Equivalent to pow(2), but more readable as a standalone call. The matrix must be square.
Returns: self - New matrix representing the square.
Throws: DomainException if the matrix is not square.
Example:
$m = Matrix::fromArray([[1, 2], [3, 4]]);
$result = $m->sqr(); // [[7, 10], [15, 22]]public function mulVector(Vector $vec): VectorMultiply the matrix by a vector (Ax). The vector is treated as a column vector; its count must equal the matrix's column count.
To go the other way (xA), use Vector::mul() instead.
Parameters:
$vec (Vector) - The vector to multiply by.
Returns: Vector - New vector representing the result.
Throws: LengthException if the vector's count doesn't equal the matrix's column count.
Examples:
$m = Matrix::fromArray([[1, 2], [3, 4]]);
$v = Vector::fromArray([1, 2]);
$result = $m->mulVector($v); // Vector(5, 11)public function t(): selfGet the transpose of the matrix (Aᵀ). Rows become columns and columns become rows.
Returns: self - New matrix representing the transpose.
Examples:
$m = Matrix::fromArray([[1, 2, 3], [4, 5, 6]]);
$transpose = $m->t();
// [[1, 4], [2, 5], [3, 6]]public function det(): floatCalculate the determinant of a matrix, which must be square.
Returns: float - The determinant.
Throws: DomainException if the matrix is not square.
Examples:
$m = Matrix::fromArray([[1, 2], [3, 4]]);
echo $m->det(); // -2.0
$idMat = Matrix::identity(3);
echo $idMat->det(); // 1.0public function trace(): floatCalculate the trace of the matrix (sum of diagonal elements). The matrix must be square.
Returns: float - The trace.
Throws: DomainException if the matrix is not square.
Examples:
$m = Matrix::fromArray([[1, 2], [3, 4]]);
echo $m->trace(); // 5.0
$idMat = Matrix::identity(3);
echo $idMat->trace(); // 3.0public function norm(): floatCalculate the Frobenius norm (square root of the sum of all squared elements). This is the matrix analogue of the Euclidean norm for vectors.
Returns: float - The Frobenius norm.
Examples:
$m = Matrix::fromArray([[1, 2], [3, 4]]);
echo $m->norm(); // 5.477... (sqrt(30))
$idMat = Matrix::identity(3);
echo $idMat->norm(); // 1.732... (sqrt(3))public function p1Norm(): floatCalculate the P1 norm (maximum absolute column sum).
Returns: float - The P1 norm.
Examples:
$m = Matrix::fromArray([[1, -2], [3, 4]]);
echo $m->p1Norm(); // 6.0 (max of |1|+|3|=4, |-2|+|4|=6)public function pInfNorm(): floatCalculate the P-infinity norm (maximum absolute row sum).
Returns: float - The P-infinity norm.
Examples:
$m = Matrix::fromArray([[1, -2], [3, 4]]);
echo $m->pInfNorm(); // 7.0 (max of |1|+|-2|=3, |3|+|4|=7)public function count(): intGet the total number of elements in the matrix (rowCount * columnCount), via the Countable interface.
Examples:
$m = new Matrix(2, 3);
echo $m->count(); // 6
echo count($m); // 6 (via the global count() function)Matrices can be accessed using bracket syntax, including chained double-index access:
$m = Matrix::fromArray([[1, 2, 3], [4, 5, 6]]);
// Read access
echo $m[0][0]; // 1
echo $m[1][2]; // 6
// Write access
$m[0][1] = 20;
echo $m[0][1]; // 20
// Set a whole row
$m[1] = Vector::fromArray([40, 50, 60]);
// Check existence
var_dump(isset($m[0])); // true
var_dump(isset($m[5])); // false
// Cannot unset rows
unset($m[0]); // Throws LogicException$m[$row] returns the Matrix's actual internal row Vector, not a copy. Mutating it mutates the Matrix. This is
what makes $m[$row][$col] = $x work: PHP fetches the row via offsetGet(), then sets the element on that same
Vector object.
This is different from getRow(), which returns an independent copy - mutating the result of getRow()
never affects the Matrix. See getRow() and setRow() below for the full contrast.
public function offsetExists(mixed $offset): boolCheck if a row offset exists. Returns true if the offset is an integer within the valid range.
Parameters:
$offset(mixed) - Row index to check.
Returns: bool - True if the offset is valid.
public function offsetGet(mixed $offset): VectorGet the row Vector at an offset. This is the Matrix's live internal row - see the note above.
Parameters:
$offset(mixed) - Row index to get.
Returns: Vector - The live row Vector.
Throws:
InvalidArgumentExceptionif the offset is not an int.OutOfRangeExceptionif the offset is outside the valid range.
public function offsetSet(mixed $offset, mixed $value): voidSet a row from a Vector. Equivalent to setRow(): the given Vector's elements are copied into the row's existing
Vector, which is never replaced.
Parameters:
$offset(mixed) - Row index to set.$value(mixed) - The row Vector.
Throws:
InvalidArgumentExceptionif the offset is not an int, or the value is not aVector.OutOfRangeExceptionif the offset is outside the valid range.LengthExceptionif the Vector has the wrong number of elements.
public function offsetUnset(mixed $offset): voidUnsetting rows is not supported.
Throws:
LogicException- Always throws.
// From constructor + set
$m = new Matrix(3, 3);
$m->set(0, 0, 1);
$m->set(1, 1, 1);
$m->set(2, 2, 1);
// From array
$m = Matrix::fromArray([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]);
// Identity shorthand
$idMat = Matrix::identity(3);// Solve Ax = b using x = A⁻¹b
$m = Matrix::fromArray([[2, 1], [5, 3]]);
$b = Vector::fromArray([4, 7]);
$x = $m->inv()->mulVector($b); // Vector(5, -6)// Rotate a point 90° around the Z-axis.
// The rotation matrix for angle θ around Z is:
// [ cos θ -sin θ 0 ]
// [ sin θ cos θ 0 ]
// [ 0 0 1 ]
$rot90 = Matrix::fromArray([
[0, -1, 0],
[1, 0, 0],
[0, 0, 1],
]);
$point = Vector::fromArray([1, 0, 0]);
$rotated = $rot90->mulVector($point); // Vector(0, 1, 0)
// Scale by 2x in all axes.
$scale = Matrix::fromArray([
[2, 0, 0],
[0, 2, 0],
[0, 0, 2],
]);
$scaled = $scale->mulVector($point); // Vector(2, 0, 0)
// Chain transformations: scale then rotate.
$combined = $rot90->mul($scale);
$result = $combined->mulVector($point); // Vector(0, 2, 0)// Fibonacci via matrix exponentiation
$fib = Matrix::fromArray([[1, 1], [1, 0]]);
$f10 = $fib->pow(10);
echo $f10->get(0, 0); // 89 (the 10th Fibonacci number)// Embed a 2x2 rotation into the top-left of a 3x3 homogeneous transform.
$rotation = Matrix::fromArray([
[0, -1],
[1, 0],
]);
$transform = Matrix::identity(3);
$transform->paste($rotation);
// [[0, -1, 0],
// [1, 0, 0],
// [0, 0, 1]]
// Extract it back out.
$extracted = $transform->copy(0, 0, 2, 2);
$rotation->equal($extracted); // true