creof/wkt-parser
A PHP library for parsing Well-Known Text (WKT) geometry strings into usable geometry objects. Supports points, linestrings, polygons, and more, making it easy to work with spatial data in Laravel and other PHP apps without heavy GIS dependencies.
The parser is a standalone, framework-agnostic library — there is no Laravel service provider, facade, config file, or auto-discovery. Wire it up yourself.
Install it:
composer require creof/wkt-parser
Look first at CrEOF\Geo\WKT\Parser (v2.x). That's the entry point; ignore anything under CrEOF\WKT\Parser\ unless you're on an old 1.x install — v1 returned typed geometry objects, v2 returns plain arrays. The array shape is documented only by example, so read Parser::parse() for the return contract.
Minimal use case — accept a WKT string from a form/API and hand it to your DB:
use CrEOF\Geo\WKT\Parser;
$parsed = (new Parser('POINT(-122.68 45.52)'))->parse();
// [
// 'type' => 'Point',
// 'value' => ['x' => -122.68, 'y' => 45.52],
// 'srid' => null,
// ]
DB::statement(
'INSERT INTO places (location) VALUES (ST_GeomFromText(?, ?))',
['POINT(-122.68 45.52)', 4326]
);
Your first meaningful use case is almost always "convert incoming WKT into something Laravel can validate, store, or serialize" — not "parse WKT in a controller." Build a small service around the parser immediately (see next section) so you don't scatter new Parser(...) calls.
Wrap the parser in a bound service. This makes it mockable, gives you one place to normalize errors, and lets you swap libraries later without touching call sites.
namespace App\Services;
use CrEOF\Geo\WKT\Parser;
use CrEOF\Geo\WKT\Exception\UnexpectedTokenException;
class WktService
{
public function parse(string $wkt): array
{
return (new Parser($wkt))->parse();
}
public function toGeoJson(string $wkt): array
{
$g = $this->parse($wkt);
return match ($g['type']) {
'Point' => ['type' => 'Point', 'coordinates' => [$g['value']['x'], $g['value']['y']]],
'LineString' => ['type' => 'LineString',
'coordinates' => array_map(fn ($p) => [$p['x'], $p['y']], $g['value'])],
'Polygon' => ['type' => 'Polygon',
'coordinates' => array_map(
fn ($ring) => array_map(fn ($p) => [$p['x'], $p['y']], $ring),
$g['value']
)],
default => throw new \InvalidArgumentException("Unsupported {$g['type']}"),
};
}
}
Bind it in a service provider: $this->app->singleton(WktService::class);
Validate with a custom rule. Never let the parser throw inside a controller — catch and convert to a validation message:
use Illuminate\Contracts\Validation\Rule;
class Wkt implements Rule
{
public function passes($attribute, $value)
{
try { (new Parser($value))->parse(); return true; }
catch (\Throwable) { return false; }
}
public function message() { return 'The :attribute must be a valid WKT/EWKT string.'; }
}
Eloquent cast for a geometry column. If your column is TEXT/GEOMETRY, a custom cast keeps models clean:
class WktCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return $value ? (new Parser($value))->parse() : null;
}
public function set($model, $key, $value, $attributes)
{
return is_string($value) ? $value : null; // accept WKT as-is
}
}
API resource transformer. For a GeoJSON FeatureCollection endpoint, extend the service above and shape responses in JsonResource classes — do not return the raw parser array. {type, value, srid} is not what Leaflet, Mapbox GL, or Mapbox/Deck consumers expect.
Batch import pipeline. When ingesting a WKT file (CSV/GeoJSON-to-WKT dumps), parse in a queued job, chunk reads at ~500 rows, and push failures to a dead-letter table. The parser is fast per-string but is not a streaming parser; do not put it inside a loop over 100k rows on a web request.
Store SRID explicitly. If you receive EWKT (SRID=4326;POINT(...)), extract $parsed['srid'] and pass it as the second arg to ST_GeomFromText. Do not default silently — mixed SRIDs in one table will bite you later.
composer.json constraints bite first). Pin PHP or plan a migration to brick/geo, geo-io/wkt-parser, or phayes/geophp. If you're already on the modern stack, treat this package as a short-term bridge only.toWkt(). You'll hand-build WKT strings or use a different library for the write path. Don't assume symmetry.Point gives ['x'=>…, 'y'=>…], LineString gives a list of those, Polygon gives a list of rings (lists of points), GeometryCollection nests one more level. There is no unifying interface — build your normalizers defensively with match or switch on type, and reject unknown types rather than passing them through.lng lat (x y), not lat/lng. WKT follows Cartesian order; GeoJSON does too, so the pass-through is fine — but don't hand a Point's x/y to a LatLng constructor expecting lat, lng. This is the single most common production bug with WKT.POINT, LINESTRING, SRID=4326;). Some downstream systems emit lowercase; the parser is not reliably tolerant. strtoupper() the type token in your service before parsing if you accept user input.CrEOF\Geo\WKT\Exception\UnexpectedTokenException (syntax) but also bare \Exception from the lexer on malformed input. Catch \Throwable at the boundary, then narrow. Never let one escape to a 500.EMPTY geometries (POINT EMPTY, POLYGON EMPTY) are inconsistently handled. Reject them at the validation layer unless your DB/SRID pipeline explicitly supports empty geometries — PostGIS does, MySQL is pickier.POINT Z (1 2 3) support is partial at best in this version. If you need 3D, verify against your version with a test fixture before relying on it; assume 2D otherwise.LINESTRING(1 2,3 4) is fine; LINESTRING(1 2 3 4) is not). Trim and canonicalize on ingest.ST_GeomFromText(wkt, srid), PostGIS wants ST_GeomFromEWKT(wkt) for EWKT or ST_GeomFromText(wkt, srid) for WKT. Pick the function based on whether srid is null in the parsed result, or normalize to EWKT first."POINT()", "SRID=abc;POINT(1 2)", "POINT(1)"). The parser's error messages are not stable across versions, so assert on exception class or null, not message text.WktService::toGeoJson() (as above), a Wkt validation rule, a WktCast, and a thin GeoJsonResource. Everything else in the package is closed — there are no pluggable visitors or handlers to hook into, so your wrapper is the extension point.How can I help you explore Laravel packages today?