rodneyrehm/plist
PHP library for reading and writing Apple property list (plist) files in both XML and binary formats. Convert plists to PHP arrays and back with a lightweight, straightforward API suited to macOS and iOS tooling, configuration, and data interchange.
Install the library directly — it has no Laravel service provider, no config file, and no facade. Everything is plain PHP objects you wire up yourself.
composer require rodneyrehm/plist
If you need the actively maintained fork (Moodle's, which backs the 2025 release), it is published under a different Composer name — pin it explicitly so you know which code you're shipping:
"repositories": [{ "type": "vcs", "url": "https://github.com/moodlehq/CFPropertyList" }],
"require": { "rodneyrehm/plist": "dev-master" }
Where to look first: src/CFPropertyList/CFPropertyList.php for the five methods that matter (parse, load, toArray, toXML/toBinary, save), then the type classes (CFDictionary, CFArray, CFString, CFInteger, CFReal, CFBoolean, CFData, CFDate, CFUid). The test suite has binary/XML fixtures you can copy into tests/fixtures.
Smoke test — read a plist and prove you can round-trip it:
use CFPropertyList\CFPropertyList;
$plist = new CFPropertyList();
$plist->parse(file_get_contents(base_path('tests/fixtures/Info.plist')));
$data = $plist->toArray(); // plain PHP array
dump($data, $plist->getFormat()); // CFPropertyList::FORMAT_XML | FORMAT_BINARY
Your first real use case is almost certainly generating .mobileconfig profiles for iOS/macOS (Wi-Fi, VPN, email, MDM enrollment) or bumping CFBundleVersion in an iOS Info.plist during CI. Both are five-line jobs once the parse/write loop clicks — start with a Wi-Fi payload and open it on a device, since that validates both structure and content type end-to-end.
Wrap it in one service class. You do not want new CFDictionary() scattered across controllers. Bind a single class (e.g. App\Support\Plist\PlistWriter) in a service provider and inject it.
Writing: build the CFType tree, not strings. There is no fromArray() inverse, so the highest-leverage thing you'll write is a recursive converter:
use CFPropertyList\{CFType, CFDictionary, CFArray, CFString, CFInteger, CFReal, CFBoolean, CFData, CFDate};
function toCFType(mixed $value): CFType
{
return match (true) {
$value instanceof CFType => $value,
is_bool($value) => new CFBoolean($value),
is_int($value) => new CFInteger($value),
is_float($value) => new CFReal($value),
$value instanceof \DateTimeInterface => new CFDate($value),
is_array($value) && array_is_list($value) => new CFArray(array_map('toCFType', $value)),
is_array($value) => tap(new CFDictionary(), function ($d) use ($value) {
foreach ($value as $k => $v) { $d->add((string) $k, toCFType($v)); }
}),
default => new CFString((string) $value),
};
}
$plist = new CFPropertyList();
$plist->add(toCFType(['PayloadType' => 'Configuration', 'PayloadVersion' => new CFInteger(1)]));
$xml = $plist->toXML(true); // pretty-printed string, no filesystem touch
Prefer toXML()/toBinary() + Laravel Storage over the library's saveXML($path)/save($path). The library's save methods use raw fopen/file_put_contents, which bypasses your disks, S3 driver, and test fakes:
Storage::disk('profiles')->put("{$uuid}.mobileconfig", $plist->toXML());
Serving a profile over HTTP — iOS is picky about the response:
return response($plist->toXML(), 200, [
'Content-Type' => 'application/x-apple-aspen-config',
'Content-Disposition' => 'attachment; filename="wifi.mobileconfig"',
]);
CI / artisan pattern — an ios:bump-build command that loads Info.plist, mutates CFBundleVersion, writes both XML and binary copies, and exits non-zero on failure. This is where the package pays for itself: no plutil, no macOS runner required.
Parsing user uploads — validate before parsing. Check the extension/mime, then guessFormat() on the raw content and reject anything that throws:
try { $plist->parse($request->file('profile')->get()); }
catch (\CFPropertyList\PListException $e) { throw ValidationException::withMessages([...]); }
Test with fixtures, assert structurally. Keep small XML and binary plists in tests/fixtures/ and assert on toArray() keys plus assertStringStartsWith('bplist00', $binary) for binary output. Round-trip tests (parse → write → parse) catch almost every serialization bug you'll hit.
[] could be <array/> or <dict/>. Decide explicitly in your converter (or pass CFArray/CFDictionary instances through) or you'll ship profiles Apple silently ignores.CFData wants raw binary, not base64. The XML writer base64-encodes for you. Passing pre-encoded data double-encodes it — a classic certificate-payload bug that only shows up on device.CFDate is built from a local-timezone DateTime, the device sees a shifted value. Normalize with ->setTimezone(new \DateTimeZone('UTC')) before wrapping.CFBoolean. 1/0 becomes <integer> and iOS treats it as truthy in some keys but not others. Your converter's is_bool branch matters more than it looks.1.0 passed as a PHP float must become CFReal to survive as <real>; PHP's loose typing will otherwise collapse it to CFInteger..mobileconfig needs CMS signing (openssl cms -sign -nodetach) done outside PHP — run it in a queued job with Process, never inline in a request.toArray() date values. Don't assume scalars — dates typically surface as DateTime objects. dd() one real profile before writing assertions.CFUid is required for NSKeyedArchiver blobs (some Apple caches/preferences). toArray() won't necessarily round-trip UIDs, so don't use the array view as your serializer for those.CFPropertyList\PListException and IOException. Catch them at the boundary and translate — a raw stack trace leaks absolute paths and looks like a crash to the user.guessFormat() on empty/whitespace content is unreliable; check strlen($content) > 8 (the bplist00 header) before parsing uploads.How can I help you explore Laravel packages today?