jeroendesloovere/vcard-bundle
Symfony bundle for generating and downloading vCard (VCF) contact files from PHP objects, wrapping the jeroendesloovere/vcard library with service integration and helpers for Symfony applications.
First, the verdict: this is a Symfony2 bundle, not a Laravel package, and it has not been touched since November 2018 (16 stars, zero dependents). Do not composer require jeroendesloovere/vcard-bundle in a Laravel app — its symfony/framework-bundle constraint is anchored to the Symfony 2.x line, which will collide with the Symfony 6/7 components that Laravel 10+ pulls in. You will get an unresolvable dependency graph before you get any value.
The bundle is a thin DI wrapper around the framework-agnostic jeroendesloovere/vcard library. That library is the thing you actually want. Install it directly:
composer require jeroendesloovere/vcard
Where to look first: src/VCard.php in the installed library plus the src/Properties/ directory. That file is the entire public surface — a builder that accumulates properties and emits a .vcf string via getOutput(). The bundle adds nothing you can't do with new VCard().
Your first use case — a "Save to contacts" endpoint. Almost every real-world use is some variant of "turn a database row into a downloadable .vcf":
// routes/web.php
Route::get('/contacts/{contact}/vcard', [ContactController::class, 'vcard']);
use JeroenDesloovere\VCard\VCard;
public function vcard(Contact $contact)
{
$vcard = new VCard();
$vcard->addName($contact->last_name, $contact->first_name);
$vcard->addCompany($contact->company);
$vcard->addEmail($contact->email, 'WORK');
$vcard->addPhoneNumber($contact->phone, 'WORK');
$vcard->addUrl($contact->website);
$raw = $vcard->getOutput();
return response($raw, 200, [
'Content-Type' => 'text/vcard; charset=utf-8',
'Content-Disposition' => 'attachment; filename="'.Str::slug($contact->full_name).'.vcf"',
'Content-Length' => strlen($raw),
]);
}
That is the 90% case. Everything below is elaboration on it.
1. Wrap the builder in a factory/service. Do not scatter new VCard() through controllers. One mapper class keeps the property list in one place and makes the output testable:
namespace App\Services;
use App\Models\Contact;
use JeroenDesloovere\VCard\VCard;
class VCardFactory
{
public function forContact(Contact $contact): VCard
{
$vcard = new VCard();
$vcard->addName($contact->last_name, $contact->first_name);
$vcard->addCompany($contact->company);
$vcard->addJobtitle($contact->job_title);
$vcard->addEmail($contact->email, 'WORK');
$vcard->addPhoneNumber($contact->phone, 'WORK');
$vcard->addAddress(null, null, $contact->street, $contact->city,
$contact->region, $contact->postal_code, $contact->country);
$vcard->addNote($contact->bio);
return $vcard;
}
}
Resolve it through the container (bind it in a service provider or just let Laravel autowire it into the controller) so the mapping is swappable per tenant/context.
2. Always build the HTTP response yourself. Use getOutput(), never the library's download() helper. Older versions of the library call PHP's header() directly and/or exit, which bypasses Laravel's response pipeline, breaks Content-Disposition filenames, and makes the route untestable. Returning response($raw, ...) gives you proper test assertions, middleware, and ETag/caching support.
3. Attach the vCard to outbound mail. Very common for sales/BD workflows — a rep's contact card in every signature:
// inside a Mailable's build() or attachments()
$this->attachData(
$this->vcardFactory->forUser($this->rep)->getOutput(),
'rep.vcf',
['mime' => 'text/vcard']
);
4. Bulk export. For "download the whole team's contacts", generate once per record and zip via ZipArchive into storage/app/tmp, then response()->download(...)->deleteFileAfterSend(true). Do the generation in a queued job if the set is large — base64 photos make each file non-trivial.
5. QR codes. getOutput() returns exactly the payload a QR encoder needs. Feed it into endroid/qr-code or a client-side library and you have a scannable "add me" card with no extra plumbing. Note that QR payload size limits (roughly 1–2 KB of practical density) mean photos are off the table here.
6. Custom/vendor properties. For anything the typed helpers don't cover — X-SOCIALPROFILE, X-ABLabel, pronouns — drop down to the generic property API (addProperty() / setProperty() depending on version). Check the README of the version you install; the v3 and v4 APIs differ.
7. Test the raw string, not the object. Snapshot assertions on the emitted text are the only reliable way to catch encoding and folding regressions:
$raw = app(VCardFactory::class)->forContact($contact)->getOutput();
$this->assertStringStartsWith("BEGIN:VCARD\r\n", $raw);
$this->assertStringContainsString("FN:John Doe", $raw);
$this->assertStringContainsString("EMAIL;TYPE=WORK:[email protected]", $raw);
setFirstName/setLastName, setFormattedName, setPhoto, etc.). Copying snippets from the bundle's README into a 4.x install will produce "call to undefined method" errors. Read the README for the exact version in your composer.lock and pin it explicitly ("jeroendesloovere/vcard": "^4.0") so a minor bump can't silently rename things under you.error_reporting appropriately in dev, and if you're stuck on an old release, prefer upgrading the standalone library over pinning your PHP version.text/vcard; charset=utf-8 is the modern choice; text/x-vcard is the legacy one Outlook sometimes expects. Serve text/vcard and test on iOS, Android, and Outlook — they disagree about almost everything else.Content-Disposition get mangled by some clients; Str::ascii(Str::slug($name)) before appending .vcf.'WORK', 'HOME', 'CELL' are the vCard 3.0 tokens; vCard 4.0 uses different casing/semantics. Passing a junk string produces a card that parses but drops the field in strict clients. Stick to the documented set.NOTE, ORG, and ADR must be backslash-escaped. The library does this, but if you ever post-process the output string (templating, string replacement), you can un-escape it and corrupt the card. Never mutate getOutput().\r\n — you'll break the fold. If you must inspect, dump to a file and open it in a real client.$contact->updated_at->timestamp and store the raw .vcf string; invalidate naturally on save.addProperty()/setProperty() API. Everything the typed helpers can't express (custom X- fields, KIND:group, BDAY with partial dates) lives there. It's the escape hatch that keeps you from forking the library.VCardFactory interface so swapping to brick/vcard or hand-rolled output later is a one-file change.How can I help you explore Laravel packages today?