├── LICENSE ├── README.md ├── composer.json └── src ├── Properties ├── Adr.php ├── Bday.php ├── Email.php ├── Gender.php ├── Kind.php ├── Member.php ├── Note.php ├── Org.php ├── Photo.php ├── Property.php ├── Role.php ├── Source.php ├── Tel.php ├── Title.php └── Url.php └── Vcard.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Astrotomic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel vCard 2 | 3 | [![Latest Version](http://img.shields.io/packagist/v/astrotomic/laravel-vcard.svg?label=Release&style=for-the-badge)](https://packagist.org/packages/astrotomic/laravel-vcard) 4 | [![MIT License](https://img.shields.io/github/license/Astrotomic/laravel-vcard.svg?label=License&color=blue&style=for-the-badge)](https://github.com/Astrotomic/laravel-vcard/blob/master/LICENSE) 5 | [![Offset Earth](https://img.shields.io/badge/Treeware-%F0%9F%8C%B3-green?style=for-the-badge)](https://plant.treeware.earth/Astrotomic/laravel-vcard) 6 | [![Larabelles](https://img.shields.io/badge/Larabelles-%F0%9F%A6%84-lightpink?style=for-the-badge)](https://www.larabelles.com/) 7 | 8 | [![phpunit](https://img.shields.io/github/workflow/status/Astrotomic/laravel-vcard/phpunit?style=flat-square&logoColor=white&logo=github&label=Tests)](https://github.com/Astrotomic/laravel-vcard/actions?query=workflow%3Aphpunit) 9 | [![pint](https://img.shields.io/github/workflow/status/Astrotomic/laravel-vcard/pint?style=flat-square&logoColor=white&logo=github&label=CS)](https://github.com/Astrotomic/laravel-vcard/actions?query=workflow%3Apint) 10 | [![Total Downloads](https://img.shields.io/packagist/dt/astrotomic/laravel-vcard.svg?label=Downloads&style=flat-square)](https://packagist.org/packages/astrotomic/laravel-vcard) 11 | 12 | A fluent builder class for vCard files. 13 | 14 | ## Installation 15 | 16 | You can install the package via composer: 17 | 18 | ```bash 19 | composer require astrotomic/laravel-vcard 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```php 25 | use Astrotomic\Vcard\Properties\Email; 26 | use Astrotomic\Vcard\Properties\Gender; 27 | use Astrotomic\Vcard\Properties\Kind; 28 | use Astrotomic\Vcard\Properties\Tel; 29 | use Astrotomic\Vcard\Vcard; 30 | use Carbon\Carbon; 31 | 32 | Vcard::make() 33 | ->kind(Kind::INDIVIDUAL) 34 | ->gender(Gender::MALE) 35 | ->fullName('John Adam Smith') 36 | ->name('Smith', 'John', 'Adam') 37 | ->email('john.smith@mail.com') 38 | ->email('john.smith@company.com', [Email::WORK, Email::INTERNET]) 39 | ->tel('+1234567890', [Tel::HOME, Tel::VOICE]) 40 | ->tel('+0987654321', [Tel::WORK, Tel::VOICE]) 41 | ->tel('+0123456789', [Tel::CELL, Tel::VOICE]) 42 | ->url('https://johnsmith.com') 43 | ->url('https://company.com') 44 | ->bday(Carbon::parse('1990-06-24')) 45 | ->adr('','','1600 Pennsylvania Ave NW', 'Washington', 'DC', '20500-0003', 'USA') 46 | ->photo('data:image/jpeg;base64,'.base64_encode(file_get_contents(__DIR__.'/stubs/photo.jpg'))) 47 | ->title('V. P. Research and Development') 48 | ->role('Excecutive') 49 | ->org('Google', 'GMail Team', 'Spam Detection Squad') 50 | ->member('john.smith@company.com', '550e8400-e29b-11d4-a716-446655440000') 51 | ->note('Hello world') 52 | ; 53 | ``` 54 | 55 | ```vcard 56 | BEGIN:VCARD 57 | VERSION:4.0 58 | FN;CHARSET=UTF-8:John Adam Smith 59 | N;CHARSET=UTF-8:Smith;John;Adam;; 60 | KIND:individual 61 | GENDER:M 62 | EMAIL;TYPE=INTERNET:john.smith@mail.com 63 | EMAIL;TYPE=WORK;TYPE=INTERNET:john.smith@company.com 64 | TEL;TYPE=HOME;TYPE=VOICE:+1234567890 65 | TEL;TYPE=WORK;TYPE=VOICE:+0987654321 66 | TEL;TYPE=CELL;TYPE=VOICE:+0123456789 67 | URL:https://johnsmith.com 68 | URL:https://company.com 69 | BDAY:1990-06-24 70 | ADR;TYPE=WORK:;;1600 Pennsylvania Ave NW;Washington;DC;20500-0003;USA 71 | PHOTO;data:image/jpeg;base64,... 72 | TITLE:V. P. Research and Development 73 | ROLE:Excecutive 74 | ORG:Google;GMail Team;Spam Detection Squad 75 | MEMBER:urn:uuid:550e8400-e29b-11d4-a716-446655440000 76 | REV:2021-02-25T10:30:45.000000Z 77 | PRODID:-//Astrotomic vCard 78 | END:VCARD 79 | ``` 80 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astrotomic/laravel-vcard", 3 | "description": "A fluent builder class for vCard files.", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "laravel", 8 | "vcard", 9 | "vcf", 10 | "contact", 11 | "contacts" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Tom Witkowski", 16 | "email": "gummibeer@astrotomic.info", 17 | "homepage": "https://gummibeer.de", 18 | "role": "Developer" 19 | } 20 | ], 21 | "homepage": "https://github.com/Astrotomic/laravel-vcard", 22 | "require": { 23 | "php": "^8.1", 24 | "astrotomic/php-conditional-proxy": "^0.2.1", 25 | "illuminate/contracts": "^9.0 || ^10.0 || ^11.0", 26 | "illuminate/http": "^9.0 || ^10.0 || ^11.0", 27 | "illuminate/support": "^9.0 || ^10.0 || ^11.0" 28 | }, 29 | "require-dev": { 30 | "laravel/pint": "^1.0", 31 | "orchestra/testbench": "^7.0 || ^8.0 || ^9.0", 32 | "phpunit/phpunit": "^9.3 || ^10.0", 33 | "spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1" 34 | }, 35 | "prefer-stable": true, 36 | "autoload": { 37 | "psr-4": { 38 | "Astrotomic\\Vcard\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Astrotomic\\Vcard\\Tests\\": "tests/" 44 | } 45 | }, 46 | "config": { 47 | "sort-packages": true 48 | }, 49 | "scripts": { 50 | "fix": "@php vendor/bin/pint" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Properties/Adr.php: -------------------------------------------------------------------------------- 1 | "TYPE={$type}", 29 | $this->types 30 | )); 31 | 32 | $parameters = implode(';', [ 33 | $this->poBox, 34 | $this->extendedAddress, 35 | $this->streetAddress, 36 | $this->locality, 37 | $this->region, 38 | $this->postalCode, 39 | $this->countryName, 40 | ]); 41 | 42 | return "ADR;{$types}:{$parameters}"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Properties/Bday.php: -------------------------------------------------------------------------------- 1 | bday->format('Y-m-d')}"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Properties/Email.php: -------------------------------------------------------------------------------- 1 | "TYPE={$type}", 19 | $this->types 20 | )); 21 | 22 | return "EMAIL;{$types}:{$this->email}"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Properties/Gender.php: -------------------------------------------------------------------------------- 1 | gender}"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Properties/Kind.php: -------------------------------------------------------------------------------- 1 | kind}"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Properties/Member.php: -------------------------------------------------------------------------------- 1 | uuid) { 17 | $member = Str::start($this->uuid, 'urn:uuid:'); 18 | } elseif ($this->email) { 19 | $member = Str::start($this->email, 'mailto:'); 20 | } else { 21 | throw new InvalidArgumentException('You have to pass at least one member identifier.'); 22 | } 23 | 24 | return "MEMBER:{$member}"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Properties/Note.php: -------------------------------------------------------------------------------- 1 | note); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Properties/Org.php: -------------------------------------------------------------------------------- 1 | company};{$this->unit};{$this->team}"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Properties/Photo.php: -------------------------------------------------------------------------------- 1 | photo}"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Properties/Property.php: -------------------------------------------------------------------------------- 1 | role}"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Properties/Source.php: -------------------------------------------------------------------------------- 1 | source}"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Properties/Tel.php: -------------------------------------------------------------------------------- 1 | "TYPE={$type}", 23 | $this->types 24 | )); 25 | 26 | return "TEL;{$types}:{$this->number}"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Properties/Title.php: -------------------------------------------------------------------------------- 1 | title}"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Properties/Url.php: -------------------------------------------------------------------------------- 1 | url}"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Vcard.php: -------------------------------------------------------------------------------- 1 | fullName = $fullName; 54 | 55 | return $this; 56 | } 57 | 58 | public function name( 59 | ?string $lastName = null, 60 | ?string $firstName = null, 61 | ?string $middleName = null, 62 | ?string $prefix = null, 63 | ?string $suffix = null 64 | ): self { 65 | $this->namePrefix = $prefix; 66 | $this->firstName = $firstName; 67 | $this->middleName = $middleName; 68 | $this->lastName = $lastName; 69 | $this->nameSuffix = $suffix; 70 | 71 | return $this; 72 | } 73 | 74 | public function email(string $email, array $types = [Email::INTERNET]): self 75 | { 76 | $this->properties[] = new Email($email, $types); 77 | 78 | return $this; 79 | } 80 | 81 | public function tel(string $number, array $types = [Tel::VOICE]): self 82 | { 83 | $this->properties[] = new Tel($number, $types); 84 | 85 | return $this; 86 | } 87 | 88 | public function url(string $url): self 89 | { 90 | $this->properties[] = new Url($url); 91 | 92 | return $this; 93 | } 94 | 95 | public function photo(string $photo): self 96 | { 97 | $this->properties[] = new Photo($photo); 98 | 99 | return $this; 100 | } 101 | 102 | public function bday(DateTimeInterface $bday): self 103 | { 104 | $this->properties[] = new Bday($bday); 105 | 106 | return $this; 107 | } 108 | 109 | public function kind(string $kind): self 110 | { 111 | $this->properties[] = new Kind($kind); 112 | 113 | return $this; 114 | } 115 | 116 | public function gender(string $gender): self 117 | { 118 | $this->properties[] = new Gender($gender); 119 | 120 | return $this; 121 | } 122 | 123 | public function org(?string $company = null, ?string $unit = null, ?string $team = null): self 124 | { 125 | $this->properties[] = new Org($company, $unit, $team); 126 | 127 | return $this; 128 | } 129 | 130 | public function title(string $title): self 131 | { 132 | $this->properties[] = new Title($title); 133 | 134 | return $this; 135 | } 136 | 137 | public function role(string $role): self 138 | { 139 | $this->properties[] = new Role($role); 140 | 141 | return $this; 142 | } 143 | 144 | public function member(?string $mail = null, ?string $uuid = null): self 145 | { 146 | $this->properties[] = new Member($mail, $uuid); 147 | 148 | return $this; 149 | } 150 | 151 | public function adr( 152 | ?string $poBox = null, 153 | ?string $extendedAddress = null, 154 | ?string $streetAddress = null, 155 | ?string $locality = null, 156 | ?string $region = null, 157 | ?string $postalCode = null, 158 | ?string $countryName = null, 159 | array $types = [Adr::WORK] 160 | ): self { 161 | $this->properties[] = new Adr( 162 | $poBox, 163 | $extendedAddress, 164 | $streetAddress, 165 | $locality, 166 | $region, 167 | $postalCode, 168 | $countryName, 169 | $types 170 | ); 171 | 172 | return $this; 173 | } 174 | 175 | public function note(string $note): self 176 | { 177 | $this->properties[] = new Note($note); 178 | 179 | return $this; 180 | } 181 | 182 | public function source(string $source): self 183 | { 184 | $this->properties[] = new Source($source); 185 | 186 | return $this; 187 | } 188 | 189 | public function __toString(): string 190 | { 191 | return collect([ 192 | 'BEGIN:VCARD', 193 | 'VERSION:4.0', 194 | "FN;CHARSET=UTF-8:{$this->getFullName()}", 195 | $this->hasNameParts() ? "N;CHARSET=UTF-8:{$this->lastName};{$this->firstName};{$this->middleName};{$this->namePrefix};{$this->nameSuffix}" : null, 196 | array_map('strval', $this->properties), 197 | sprintf('REV:%s', Carbon::now()->toISOString()), 198 | 'PRODID:-//Astrotomic vCard', 199 | 'END:VCARD', 200 | ])->flatten()->filter()->implode(PHP_EOL); 201 | } 202 | 203 | public function toResponse($request) 204 | { 205 | $content = strval($this); 206 | 207 | $filename = Str::of($this->getFullName())->slug('_')->append('.vcf'); 208 | 209 | return new Response($content, 200, [ 210 | 'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0', 211 | 'Content-Type' => 'text/vcard', 212 | 'Content-Length' => strlen($content), 213 | 'Content-Disposition' => HeaderUtils::makeDisposition( 214 | HeaderUtils::DISPOSITION_ATTACHMENT, 215 | $filename, 216 | $filename->ascii()->replace('%', '') 217 | ), 218 | ]); 219 | } 220 | 221 | protected function getFullName(): string 222 | { 223 | return $this->fullName ?? collect([ 224 | $this->namePrefix, 225 | $this->firstName, 226 | $this->middleName, 227 | $this->lastName, 228 | $this->nameSuffix, 229 | ])->filter()->implode(' '); 230 | } 231 | 232 | protected function hasNameParts(): bool 233 | { 234 | return ! empty(array_filter([ 235 | $this->namePrefix, 236 | $this->firstName, 237 | $this->middleName, 238 | $this->lastName, 239 | $this->nameSuffix, 240 | ])); 241 | } 242 | } 243 | --------------------------------------------------------------------------------