creof/wkb-parser
A lightweight PHP parser for Well-Known Binary (WKB) geometry data. It converts WKB/EWKB strings into usable PHP geometry objects, making it easy to work with spatial data from databases and GIS tools without heavy dependencies.
creof/wkb-parser is a small, focused reader: you give it WKB/EWKB bytes, it gives you back geometry value objects. There is no encoder, no database layer, and no GeoJSON serializer bundled — treat it as the "decode" half of a geometry pipeline.
Install it with Composer:
composer require creof/wkb-parser
The first place to look is the installed source, not the README, because the package is small and the README is thin:
ls vendor/creof/wkb-parser/src
sed -n '1,200p' vendor/creof/wkb-parser/src/CrEOF/WKB/Parser.php
Check two things in particular: the exact namespace of Parser, and whether parse() expects raw binary or a hex string in your installed version. Then confirm the geometry classes returned (the package depends on/returns geometry objects from the same author's geo namespace — verify the class names with get_class($result) on your first successful parse).
A minimal first use case — decode a PostGIS hex WKB value and inspect the resulting geometry:
use CrEOF\WKB\Parser;
// WKB for POINT(1 1), hex-encoded (as PostGIS often returns it)
$hex = '0101000000000000000000f03f000000000000f03f';
$wkb = hex2bin($hex);
$parser = new Parser();
$geometry = $parser->parse($wkb);
var_dump(get_class($geometry)); // confirm class/namespace for your version
If that works, the day-to-day use is almost always: fetch a geometry column from PostGIS/MySQL, decode it, and hand the coordinates to your application or an API response. If parse() fails on the binary string, pass the hex string directly instead — see Gotchas.
Decode PostGIS output inside a repository. The clean pattern is to keep parsing at the data-access boundary so the rest of the app never sees WKB:
final class PlaceRepository
{
public function __construct(
private PDO $pdo,
private Parser $parser
) {}
public function find(int $id): ?Place
{
$stmt = $this->pdo->prepare(
'SELECT id, name, ST_AsBinary(location) AS location_wkb
FROM places WHERE id = :id'
);
$stmt->execute(['id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
return new Place(
$row['id'],
$row['name'],
$this->parser->parse($row['location_wkb'])
);
}
}
ST_AsBinary (not ST_AsText) keeps the payload smaller and avoids the WKT parser dependency. Because Parser is stateless in normal use, register one instance in your container and inject it.
Use EWKB when SRID matters. PostGIS ST_AsEWKB embeds the SRID; plain ST_AsBinary does not. Use ST_AsEWKB if downstream code (transformations, storage) needs to know which coordinate system the geometry belongs to:
$stmt = $this->pdo->query('SELECT ST_AsEWKB(location) FROM places');
$geometry = $parser->parse($stmt->fetchColumn()); // carries SRID
Keep the WKB payload binary and store the SRID separately (ST_SRID(location)) if you want to be robust against parser version differences in EWKB flag handling.
Normalize hex/binary input at the edge. Many drivers hand back hex or PostGIS bytea escapes. Wrap the parser so callers always pass a clean binary string:
function decodeWkb(Parser $parser, string|resource $value): object
{
if (is_resource($value)) {
$value = stream_get_contents($value); // PDO pgsql bytea as a stream
}
// PostGIS bytea hex escape: \x0101000000...
if (str_starts_with($value, '\\x')) {
$value = substr($value, 2);
}
// Heuristic: WKB's first byte is 0x00/0x01 (endianness).
// Anything longer and all-hex is probably hex-encoded.
if (strlen($value) > 2 && ctype_xdigit($value)) {
$value = hex2bin($value);
}
return $parser->parse($value);
}
Convert to GeoJSON at the API boundary. The package returns geometry objects, not arrays. Write one explicit adapter and test it heavily; this is where you map each geometry type to {type, coordinates} and recurse into GeometryCollection:
function toGeoJson($geometry): array
{
// switch on the type accessor exposed by the returned geometry class,
// e.g. Point -> [x, y], LineString -> [[x,y],...], etc.
// Recurse for Multi* and GeometryCollection.
}
Keeping this conversion in one place means upgrades or a parser swap only break the adapter, not every controller.
Custom Doctrine type. If you use Doctrine, map geometry columns to a custom type whose convertToPHPValue calls the parser and whose convertToDatabaseValue emits EWKB (or, better, defers to PostGIS functions in the query). This keeps entity code free of Parser calls and centralizes hex/bytea normalization.
Batch parsing. When exporting many rows, reuse a single Parser instance, wrap each parse in try/catch so one malformed geometry doesn't abort the export, and log the offending hex with its primary key. WKB is compact but a large polygon collection can still hold a lot of coordinates — stream rows rather than fetchAll.
parse() expects by parsing a known one-point fixture both ways. The first byte of real WKB is 0x00 or 0x01; if the first character is '0' and the string is all hex digits, you almost certainly have hex.pgsql PDO returns bytea as a stream resource on some setups and as a \x...-escaped string on others. Normalize both before parsing. Avoid PDO::ATTR_STRINGIFY_FETCHES surprises when comparing test fixtures.0x80000000 = has SRID, 0x40000000 = Z, 0x20000000 = M. If coordinates come back shifted or truncated, dump the type word and decode the flags manually — a type of 0x20000001 is not a typo.GEOMETRYCOLLECTION EMPTY, POINT EMPTY, and SQL NULL arrive differently. Handle NULL before parsing and add fixtures for empty geometries; don't let a NULL column reach parse().composer.json, and run your PHP 8.2/8.3 matrix early. If it breaks, the parser is small enough to fork or vendor, or consider creof/geo-parser / an actively maintained alternative. Abstract it behind a GeometryDecoder interface so the swap is cheap.parse() in try/catch and convert exceptions into your own domain exception that includes a hex excerpt and the source row id. Raw parser exceptions are hard to trace back to a record.Parser, build thin wrappers for input normalization, GeoJSON conversion, and Doctrine integration. That keeps you compatible with upstream changes and gives you clear seams for tests.How can I help you explore Laravel packages today?