Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Kml Parser Laravel Package

plin-code/kml-parser

Parse KML and KMZ files in Laravel to extract placemarks, styles, style maps, and document metadata. Simple installation, configurable namespace and temp directory, with a clean API for reading geographic data.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config

composer require plin-code/kml-parser
php artisan vendor:publish --tag="kml-parser-config"

Publish even if you change nothing. The README's config snippet is stale: it predates the KMZ archive ceilings added in 3.0, so the file you actually get contains keys the docs never show. Read the published config/kml-parser.php and treat it as authoritative.

Where to look first

  1. config/kml-parser.php — namespace (leave at http://www.opengis.net/kml/2.2 unless a vendor ships a non-standard dialect), temp_directory for KMZ scratch extraction, and the archive limits.
  2. The 3.1.0 release notes, not the README. They document the folder key on placemarks, the corrected unreadable-file error, and the PHPStan level 9 typing pass — all of which change how you consume return values.
  3. PlinCode\KmlParser\KmlValidator plus the exception tree (KmlException → KmlParserException / KmzExtractorException). That is your input contract.

The whole public surface is small: loadFromFile(), loadFromKmz(), loadFromString(), then getPlacemarks(), getStyles(), getStyleMaps(), getDocumentName(), getDocumentDescription(), toGeoJson(). No models, no migrations, no stored state to manage.

First use case: ingest a user-uploaded map

use PlinCode\KmlParser\KmlParser;
use PlinCode\KmlParser\Exceptions\KmlParserException;

$parser = new KmlParser();

try {
    str_ends_with(strtolower($path), '.kmz')
        ? $parser->loadFromKmz($path)
        : $parser->loadFromFile($path);
} catch (KmlParserException $e) {
    report($e);
    return back()->withErrors(['file' => 'That map file could not be read.']);
}

foreach ($parser->getPlacemarks() as $placemark) {
    ImportedPoint::create([
        'name'       => $placemark['name'] ?? null,
        'layer'      => implode('/', $placemark['folder']), // ['Piemonte', 'Laghi']
        'type'       => $placemark['type'] ?? null,        // Point, LineString, MultiGeometry…
        'coordinates'=> $placemark['coordinates'] ?? null,
    ]);
}

Everything downstream is identical for KMZ; only the loader call differs. The facade (KmlParser::loadFromFile(...)->getPlacemarks()) is convenient in tinker and one-liners, but prefer new KmlParser() in application code — see the gotchas.

Implementation Patterns

One service, one door. Funnel the extension check, size guard, and try/catch through a single KmlImportService used by controllers, commands, and jobs. Return your own DTO rather than passing raw arrays around — the maintainer explicitly keeps placemarks as a heterogeneous map, so the shape is not enforceable by the type system. Normalise once at the boundary:

final class KmlImportService
{
    public function import(string $absolutePath): ParsedDocument
    {
        $parser = new KmlParser();
        str_ends_with(Str::lower($absolutePath), '.kmz')
            ? $parser->loadFromKmz($absolutePath)
            : $parser->loadFromFile($absolutePath);

        return new ParsedDocument(
            name:        $parser->getDocumentName(),
            description: $parser->getDocumentDescription(),
            placemarks:  $parser->getPlacemarks(),
        );
    }
}

Parse off the request thread. The parser loads the document into a DOM and runs a //kml:Placemark XPath over it. There is no streaming path. A 20 MB KMZ is a request timeout and a trivial DoS if user-supplied. Queue it and enforce a byte ceiling at upload validation:

final class ImportKmlFile implements ShouldQueue
{
    public int $timeout = 120;

    public function __construct(public string $diskPath) {}

    public function handle(KmlImportService $importer): void
    {
        $importer->import(Storage::path($this->diskPath));
    }
}

Group by layer without extra queries. The folder path is returned outermost-first and the list stays flat, so grouping is a one-liner — ideal for rebuilding a Google My Maps export as nested UI:

$layers = collect($parser->getPlacemarks())
    ->groupBy(fn (array $p) => implode('/', $p['folder']));

Feed maps directly with GeoJSON. toGeoJson() is the shortest path to a Leaflet/Mapbox frontend; folder paths land in properties for foldered placemarks only.

return response()->json(json_decode($parser->toGeoJson()));

KMZ assets. If the archive carries icons or overlays you actually need, KmzExtractor::extractAllFiles($kmz, $dir) unpacks everything to a directory — then copy what you need into your own storage disk rather than serving from the temp dir.

Seed and test with fixtures. Keep real .kml/.kmz samples in tests/Fixtures/ — a flat document, a foldered one, a MultiGeometry, and a deliberately corrupt file for the exception path. Assert on individual keys ($placemark['name']), never on whole-array identity.

Isolate the dependency. Three stars and zero dependents means it can be abandoned. Put KmlImportService behind an interface so swapping in a fork or a different parser is a one-class change.

Gotchas and Tips

  • folder is new in 3.1.0. Placemarks gained the key, so a === or Pest toBe() comparison against a full array will fail. Reading individual keys is unaffected. It is an empty array for placemarks directly under Document, and contains an empty string for unnamed folders — path length always equals nesting depth.
  • The README config block is not what you get. The archive ceilings from 3.0 are missing from it. Inspect the published file.
  • Non-numeric archive limits now fall back to the default instead of disabling the limit (3.1.0 fixed (int) 'plenty' === 0, which silently switched protection off). Don't rely on 0 meaning "no limit" unless the config comment says so for that key.
  • Two distinct file errors. Unable to read KML file: <path> means permissions, mid-write, or a broken symlink; a missing file has its own message. Both are worth surfacing differently to users.
  • Validation is wired into loadFromString(). Don't assume the file/KMZ loaders give you identical pre-validation — catch KmlParserException around every entry point regardless.
  • Catch the specific exception types. The hierarchy is KmlException → KmlParserException / KmzExtractorException. Catching the base class hides which stage failed; keep the KMZ extraction failure separate if you surface retry semantics.
  • Reuse the same parser instance carefully. It holds state from the last load. Construct a fresh new KmlParser() per document, especially in queue workers and Octane, where a resolved facade instance can outlive one request.
  • Expect gaps in KML coverage. This is a young parser; google Earth extensions (gx:Track, gx:coord), ExtendedData custom fields, and time spans are not part of the documented surface. If your source files come from Google Earth rather than My Maps, verify against a real fixture before committing to the package.
  • Styles are returned, not resolved. getStyles()/getStyleMaps() give you the declarations and styleUrl references; there is no automatic join back onto placemarks. Build the lookup yourself if you need icon colours or line widths in your UI.
  • coordinates is absent on MultiGeometry — that's precisely the key PHPStan level 9 could not guarantee. Always use ?? null / isset() on geometry keys.
  • Clean up KMZ temp dirs. temp_directory defaults to the system temp path; extracted files linger. Extract into a per-import directory and delete it in a finally, or point the config at a directory your own cleanup command sweeps.
  • Pin the version. With v3.1.0 landing as recently as it did and a small user base, use a caret constraint and read each changelog — 3.1.0 was additive only, and the maintainer was explicit about that being a deliberate policy.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo