A KMZ file is a ZIP archive containing a KML document and any resources referenced by it. Google Earth expects the main document to be named doc.kml at the archive root. Images can live in subdirectories as long as their KML references use the same relative paths.
The following PHP function packages one generated KML document together with every PNG icon from a local directory:
<?php
function createKmz(string $outputPath, string $iconDirectory): void
{
$kml = <<<KML
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<name>Example map</name>
<Style id="marker">
<IconStyle>
<Icon>
<href>files/marker.png</href>
</Icon>
</IconStyle>
</Style>
<Placemark>
<name>Example location</name>
<description>Created dynamically with PHP.</description>
<styleUrl>#marker</styleUrl>
<Point>
<coordinates>37.545734,14.159431,0</coordinates>
</Point>
</Placemark>
</Document>
</kml>
KML;
$zip = new ZipArchive();
$result = $zip->open(
$outputPath,
ZipArchive::CREATE | ZipArchive::OVERWRITE
);
if ($result !== true) {
throw new RuntimeException(
"Unable to create KMZ archive (ZipArchive error {$result})"
);
}
try {
if (!$zip->addFromString('doc.kml', $kml)) {
throw new RuntimeException('Unable to add doc.kml');
}
foreach (glob($iconDirectory . '/*.png') ?: [] as $file) {
$archivePath = 'files/' . basename($file);
if (!$zip->addFile($file, $archivePath)) {
throw new RuntimeException("Unable to add {$file}");
}
}
} finally {
if (!$zip->close()) {
throw new RuntimeException('Unable to finalize KMZ archive');
}
}
}
createKmz(__DIR__ . '/GoogleEarth.kmz', __DIR__ . '/icons'); Archive Layout
With an icons/marker.png source file, the resulting archive contains:
GoogleEarth.kmz
├── doc.kml
└── files/
└── marker.png The KML reference files/marker.png is relative to doc.kml, so it resolves inside the archive. ZIP entries always use forward slashes, even when the PHP code runs on Windows.
Important Details
- The PHP ZIP extension must be installed and enabled; verify it with
php -m | grep zip. ZipArchive::open()returnstrueon success and an integer error code on failure. A strict comparison is required because a nonzero error code is truthy in PHP.ZipArchive::OVERWRITEreplaces an existing output archive instead of retaining stale entries from an earlier run.- KML coordinates use longitude, latitude, altitude order, not latitude, longitude.
- Dynamic text inserted into KML must be XML-escaped, for example with
htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8').
For a download endpoint, write the archive to a temporary file, send it with the media type application/vnd.google-earth.kmz, and delete the temporary file after the response has been transmitted.