spraed/vcard-bundle
Symfony bundle for creating and parsing vCard (VCF) files. It provides services and helpers to build electronic business cards from PHP objects, making it easy to export contacts for import into address books, phones, and other applications.
The first thing to understand is that this is not a Laravel package. spraed/vcard-bundle is a Symfony bundle: its entire job is to register the underlying spraed/vcard exporter inside Symfony's DI container and expose it via spraed_vcard service aliases. In Laravel there is no service provider, no facade, no config file to publish, and its Bundle class will do nothing for you. Pulling this into a Laravel app only makes sense as a source of the exporter API; the bundle wiring is dead weight.
Given it was archived in 2019, do this instead:
composer require spraed/vcard
Then look at these three classes first, in this order:
Spraed\Vcard\VCardExporter — the actual serialiser.Spraed\Vcard\VCardBuilder — the fluent builder if you do not want annotations.@VCard, @VCardName, @VCardEmail, @VCardTelephone, etc.) — only if you plan to map DTOs.Your first concrete use case should be a single "download contact" endpoint: a controller returning a .vcf file for one record. Something like:
Route::get('/customers/{customer}/vcard', function (Customer $customer) {
$vcard = (new VCardBuilder())
->setFirstName($customer->first_name)
->setLastName($customer->last_name)
->setEmail($customer->email)
->setPhone($customer->phone)
->build();
return response($vcard, 200, [
'Content-Type' => 'text/vcard; charset=utf-8',
'Content-Disposition' => 'attachment; filename="contact.vcf"',
]);
});
If that works end-to-end in one endpoint you have validated most of what you need.
Because there is no framework integration for Laravel, the reliable pattern is to keep the library at arm's length behind a thin, app-owned service.
Wrap the builder, don't scatter it. Create App\Services\VCardService with methods like fromCustomer(Customer $c): string. This gives you one place to swap libraries later (see Gotchas) and one place to handle encoding and escaping.
Use the builder for Eloquent-driven flows; use annotations only for stable DTOs. The annotation approach couples your serialisation to property docblocks, which is fragile against a codebase that lives in IDE refactors and static analysis. If you must use annotations, define a plain CustomerVCard DTO and populate it from the model rather than annotating the Eloquent model directly. That keeps presentation concerns out of your persistence layer.
Controller pattern for lists. For a "export all contacts" feature, loop over a collection and concatenate — vCard files are just concatenated cards with no wrapper:
$body = $customers
->map(fn ($c) => app(VCardService::class)->fromCustomer($c))
->implode("\r\n");
QR-code integration. A common day-job requirement is putting a contact into a QR code. Build the vCard string, then feed it into a QR library (e.g. bacon/bacon-qr-code). The builder API is well suited to this because you control exactly which fields are present and can trim optional fields to keep the payload small.
Mailing / attachment flows. For sending a .vcf as an email attachment, use the returned string directly with Laravel's mail Attachment::fromData(fn () => $vcard, 'contact.vcf')->withMime('text/vcard'). No temp files needed.
Do not try to register the bundle. There is no AppServiceProvider hook that helps; Symfony bundles rely on Bundle subclasses and YAML/XML service definitions that Laravel does not read. If you are stuck on the bundle because of a dependency, isolate the exporter in a small adapter class and never let the Bundle class touch your container.
vCard 2.1 is genuinely old. Some modern clients (Apple Contacts, Google Contacts) handle it, but 3.0 and 4.0 are the mainstream targets now. Expect field mismatches on newer clients — for example, some clients only pick up certain address labels in 2.1, and photo encoding differences bite. If the client audience is consumer-facing, seriously consider a vCard 3.0/4.0 library.
Character encoding is the number-one bug source. vCard 2.1 predates UTF-8 as a default and expects quoted-printable encoded bodies and CRLF line endings. Non-ASCII names (accented, CJK, emoji) are where things break silently. Always verify output with:
dump(app(VCardService::class)->fromCustomer($customer));
and check that it contains CHARSET=UTF-8 where appropriate and that lines are CRLF-delimited.
Line folding. vCard 2.1 requires lines folded at 75 octets. If your builder writes a long free-text note, check whether the output is folded correctly — some clients reject unfolded files.
PHP version drift is the big installation risk. Last release was Nov 2019. That predates PHP 8.0 by months. If the dependency chain includes doctrine/annotations, know that annotations are soft-deprecated and the ecosystem has been migrating to attributes. On PHP 8.3+ you may hit deprecation noise from transitive deps even if the core library works. Check composer why-not php if install fails.
The bundle's Bundle class is a red herring. If you installed the bundle package and see no service, that is expected — the wiring only happens in a Symfony kernel. Use the underlying library directly.
Escaping is your responsibility. Semicolons, commas, and newlines in names and addresses will corrupt the output if the builder does not escape them. Add a small normaliser in your VCardService that strips \r\n, and test with a deliberately nasty value like Last, First; \n.
Extension points are essentially nil. There is no config, no event system, and no plugin hooks. Custom fields (e.g. X- prefixed properties) require constructing lines yourself and appending to the output string.
Evaluate alternatives before committing. Given the package is archived with a single star and no dependents, plan an escape hatch. jeroendesloovere/vcard has a 3.0/4.0-capable API and is actively maintained; hand-rolling a small builder (~200 lines) is also realistic for simple contact cards and gives you control over encoding, which is where most of the pain lies. The wrapper-service pattern above means replacing spraed/vcard is a one-file change.
Debugging tip: test with a real client. Generate the file, then import it into Google Contacts, Apple Contacts, and a phone's native import. Output that looks correct in a text editor frequently fails to import because of a missing VERSION line, wrong CRLF, or an undeclared charset. Keep a tiny fixture endpoint that regenerates a known-bad-contact card so you can iterate quickly.
How can I help you explore Laravel packages today?