From a5043985fc9abbce5ac9b316d8815d836415f2ae Mon Sep 17 00:00:00 2001 From: Maximilian Graf Schimmelmann Date: Mon, 8 Jan 2024 17:37:23 +0100 Subject: [PATCH 1/6] Deprecated PHP7, implemented types for methods, implemented phpcs, implemented phpstan, implemented github actions, implemented DTOs. --- .github/workflows/ci.yaml | 59 +++ .gitignore | 3 +- README.md | 20 +- Tests/VCardExceptionTest.php | 17 + Tests/VCardParserTest.php | 313 +++++++++++++ Tests/VCardTest.php | 243 +++++++++++ {tests => Tests}/empty.jpg | 0 {tests => Tests}/emptyfile | 0 {tests => Tests}/example.vcf | 0 {tests => Tests}/image.jpg | Bin {tests => Tests}/wrongfile | 0 build/php/Dockerfile | 17 + build/php/etc/php.ini | 25 ++ composer.json | 23 +- docker-compose.yaml | 7 + ecs.php | 276 ++++++++++++ examples/example.php | 7 +- examples/example_parsing.php | 8 +- phpstan.neon | 6 + phpunit.xml.dist | 21 +- src/Dto/AddressData.php | 67 +++ src/Dto/CardData.php | 286 ++++++++++++ src/VCard.php | 819 ++++++++++------------------------- src/VCardException.php | 24 +- src/VCardInterface.php | 108 +++++ src/VCardParser.php | 432 ++++++++---------- tests/VCardExceptionTest.php | 24 - tests/VCardParserTest.php | 296 ------------- tests/VCardTest.php | 466 -------------------- 29 files changed, 1892 insertions(+), 1675 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 Tests/VCardExceptionTest.php create mode 100644 Tests/VCardParserTest.php create mode 100644 Tests/VCardTest.php rename {tests => Tests}/empty.jpg (100%) rename {tests => Tests}/emptyfile (100%) rename {tests => Tests}/example.vcf (100%) rename {tests => Tests}/image.jpg (100%) rename {tests => Tests}/wrongfile (100%) create mode 100644 build/php/Dockerfile create mode 100644 build/php/etc/php.ini create mode 100644 docker-compose.yaml create mode 100644 ecs.php create mode 100644 phpstan.neon create mode 100644 src/Dto/AddressData.php create mode 100644 src/Dto/CardData.php create mode 100644 src/VCardInterface.php delete mode 100644 tests/VCardExceptionTest.php delete mode 100644 tests/VCardParserTest.php delete mode 100644 tests/VCardTest.php diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..3927e05 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,59 @@ +name: "Test Pipeline" + +on: + pull_request: + push: + branches: + - "master" + +jobs: + tests: + name: "Run Tests" + runs-on: "ubuntu-latest" + strategy: + matrix: + php-version: + - "8.1" + - "8.2" + - "8.3" + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "pcov" + php-version: "${{ matrix.php-version }}" + ini-values: memory_limit=-1 + tools: composer:v2, cs2pr + + - name: "Cache dependencies" + uses: "actions/cache@v2" + with: + path: | + ~/.composer/cache + vendor + key: "php-${{ matrix.php-version }}" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress --no-suggest" + + - name: "EasyCodingStandards for src" + run: "vendor/bin/ecs check src/ tests/ --no-interaction --no-progress-bar" + + - name: "PhpStan for src/" + run: "vendor/bin/phpstan analyse --error-format=checkstyle src --level=8 | cs2pr" + + - name: "PhpStan for tests/" + run: "vendor/bin/phpstan analyse --error-format=checkstyle tests --level=6 | cs2pr" + + - name: "PHPUnit Test with Coverage" + run: "vendor/bin/phpunit -c phpunit.xml.dist tests/ --coverage-clover=clover.xml" + +# - name: Upload coverage reports to Codecov +# uses: codecov/codecov-action@v3 +# with: +# file: src/clover.xml +# env: +# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 552aa2d..81f2ef1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ composer.lock vendor .idea/ -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache +.phpunit.cache diff --git a/README.md b/README.md index c41e4dc..d24ea19 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ $vcard->addRole('Data Protection Officer'); $vcard->addEmail('info@jeroendesloovere.be'); $vcard->addPhoneNumber(1234121212, 'PREF;WORK'); $vcard->addPhoneNumber(123456789, 'WORK'); -$vcard->addAddress(null, null, 'street', 'worktown', null, 'workpostcode', 'Belgium'); +$vcard->addAddress('', '', 'street', 'worktown', '', 'workpostcode', 'Belgium'); $vcard->addLabel('street, worktown, workpostcode Belgium'); -$vcard->addURL('http://www.jeroendesloovere.be'); +$vcard->addURL('https://www.wikipedia.de'); $vcard->addPhoto(__DIR__ . '/landscape.jpeg'); @@ -60,7 +60,7 @@ return $vcard->download(); ``` -> [View all examples](/examples/example.php) or check [the VCard class](/src/VCard.php). +> [View all examples](/examples/example.php) or check [the VCard class](/src_/VCard.php). ### Parsing examples @@ -78,9 +78,9 @@ Or by using a factory method with a file name: ```php $parser = VCardParser::parseFromFile('path/to/file.vcf'); -echo $parser->getCardAtIndex(0)->fullname; // Prints the full name. +echo $parser->getCardAtIndex(0)->getName(); // Prints the full name. ``` -> [View the parsing example](/examples/example_parsing.php) or check the [the VCardParser class](/src/VCardParser.php) class. +> [View the parsing example](/examples/example_parsing.php) or check the [the VCardParser class](/src_/VCardParser.php) class. **Support for frameworks** @@ -127,6 +127,16 @@ Contributions are **welcome** and will be fully **credited**. More info on how to work with GitHub on help.github.com. +### Development + +In order to run the development instance of this repository, you can very easily utilize the shipped docker-compose package. + +```bash +docker compose up --build +``` + +Afterwards you will have an instance with PHP8.3 running that you can use to develop your changes. Xdebug is enabled by default, so you can use your IDE to debug the code. + ## Credits - [Jeroen Desloovere](https://github.com/jeroendesloovere) diff --git a/Tests/VCardExceptionTest.php b/Tests/VCardExceptionTest.php new file mode 100644 index 0000000..15277af --- /dev/null +++ b/Tests/VCardExceptionTest.php @@ -0,0 +1,17 @@ +expectException(VCardException::class); + + throw new VCardException('Testing the VCard error.'); + } +} diff --git a/Tests/VCardParserTest.php b/Tests/VCardParserTest.php new file mode 100644 index 0000000..56940fb --- /dev/null +++ b/Tests/VCardParserTest.php @@ -0,0 +1,313 @@ +vcard = new VCard(); + } + + public function test_it_will_throw_an_exception_when_wrong_index_is_requested(): void + { + $this->expectException(InvalidArgumentException::class); + $parser = new VCardParser(''); + $parser->getCardAtIndex(2); + } + + public function test_it_can_correctly_transform_a_simple_vcard(): void + { + $this->vcard->addName('Desloovere', 'Jeroen'); + $parser = new VCardParser($this->vcard->buildVCard()); + $this->assertSame($parser->getCardAtIndex(0)->getFirstName(), 'Jeroen'); + $this->assertSame($parser->getCardAtIndex(0)->getLastName(), 'Desloovere'); + $this->assertSame($parser->getCardAtIndex(0)->getName(), 'Jeroen Desloovere'); + } + + public function test_it_can_retrieve_the_birthday_in_the_right_format(): void + { + $date = new DateTimeImmutable('01-01-2021'); + $this->vcard->addBirthday($date); + $parser = new VCardParser($this->vcard->buildVCard()); + $this->assertSame($parser->getCardAtIndex(0)->getBirthday()->format('Y-m-d H:i:s'), $date->format('Y-m-d H:i:s')); + } + + public function test_it_can_parse_addresses_correctly(): void + { + $this->vcard->addAddress( + 'Lorem Corp.', + '(extended info)', + '54th Ipsum Street', + 'PHPsville', + 'Guacamole', + '01158', + 'Gitland', + ); + + $this->vcard->addAddress( + 'Jeroen Desloovere', + '(extended info, again)', + '25th Some Address', + 'Townsville', + 'Area 51', + '045784', + 'Europe (is a country, right?)', + ['WORK', 'PERSONAL'], + ); + + $this->vcard->addAddress( + 'Georges Desloovere', + '(extended info, again, again)', + '26th Some Address', + 'Townsville-South', + 'Area 51B', + '04554', + "Europe (no, it isn't)", + ['WORK', 'PERSONAL'], + ); + + $parser = new VCardParser($this->vcard->buildVCard()); + $resolve = $parser->getCardAtIndex(0)->getAddress(); + $this->assertSame($resolve['WORK;POSTAL'][0]->toArray(), [ + 'name' => 'Lorem Corp.', + 'extended' => '(extended info)', + 'street' => '54th Ipsum Street', + 'city' => 'PHPsville', + 'region' => 'Guacamole', + 'zip' => '01158', + 'country' => 'Gitland', + ]); + + $this->assertSame($resolve['WORK;PERSONAL'][0]->toArray(), [ + 'name' => 'Jeroen Desloovere', + 'extended' => '(extended info, again)', + 'street' => '25th Some Address', + 'city' => 'Townsville', + 'region' => 'Area 51', + 'zip' => '045784', + 'country' => 'Europe (is a country, right?)', + ]); + + $this->assertSame($resolve['WORK;PERSONAL'][1]->toArray(), [ + 'name' => 'Georges Desloovere', + 'extended' => '(extended info, again, again)', + 'street' => '26th Some Address', + 'city' => 'Townsville-South', + 'region' => 'Area 51B', + 'zip' => '04554', + 'country' => "Europe (no, it isn't)", + ]); + } + + public function test_it_can_set_and_get_multiple_phone_numbers(): void + { + $this->vcard->addPhoneNumber('0984456123'); + $this->vcard->addPhoneNumber('2015123487', ['WORK']); + $this->vcard->addPhoneNumber('4875446578', ['WORK']); + $this->vcard->addPhoneNumber('9875445464', ['PREF', 'WORK', 'VOICE']); + + $parser = new VCardParser($this->vcard->buildVCard()); + $resolve = $parser->getCardAtIndex(0)->getPhone(); + $this->assertSame($resolve['default'][0], '0984456123'); + $this->assertSame($resolve['WORK'][0], '2015123487'); + $this->assertSame($resolve['WORK'][1], '4875446578'); + $this->assertSame($resolve['PREF;WORK;VOICE'][0], '9875445464'); + } + + public function test_it_can_get_and_set_the_emails_correctly(): void + { + $this->vcard->addEmail('some@email.com'); + $this->vcard->addEmail('site@corp.net', ['WORK']); + $this->vcard->addEmail('site.corp@corp.net', ['WORK']); + $this->vcard->addEmail('support@info.info', ['PREF', 'WORK']); + $parser = new VCardParser($this->vcard->buildVCard()); + $resolve = $parser->getCardAtIndex(0)->getEmails(); + $this->assertSame($resolve['INTERNET'][0], 'some@email.com'); + $this->assertSame($resolve['INTERNET;WORK'][0], 'site@corp.net'); + $this->assertSame($resolve['INTERNET;WORK'][1], 'site.corp@corp.net'); + $this->assertSame($resolve['INTERNET;PREF;WORK'][0], 'support@info.info'); + } + + public function test_it_can_get_and_set_the_org_correctly(): void + { + $this->vcard->addCompany('Lorem Corp.'); + $parser = new VCardParser($this->vcard->buildVCard()); + $resolve = $parser->getCardAtIndex(0)->getOrganization(); + $this->assertSame($resolve, 'Lorem Corp.'); + } + + public function test_it_can_get_and_set_multiple_urls_correctly(): void + { + $this->vcard->addUrl('http://www.jeroendesloovere.be'); + $this->vcard->addUrl('http://home.example.com', 'HOME'); + $this->vcard->addUrl('http://work1.example.com', 'PREF;WORK'); + $this->vcard->addUrl('http://work2.example.com', 'PREF;WORK'); + $parser = new VCardParser($this->vcard->buildVCard()); + $resolve = $parser->getCardAtIndex(0)->getUrls(); + $this->assertSame($resolve['default'][0], 'http://www.jeroendesloovere.be'); + $this->assertSame($resolve['HOME'][0], 'http://home.example.com'); + $this->assertSame($resolve['PREF;WORK'][0], 'http://work1.example.com'); + $this->assertSame($resolve['PREF;WORK'][1], 'http://work2.example.com'); + } + + public function test_it_can_set_and_get_the_cards_note_correctly(): void + { + $this->vcard->addNote('This is a testnote'); + $parser = new VCardParser($this->vcard->buildVCard()); + + $vcardMultiline = new VCard(); + $vcardMultiline->addNote("This is a multiline note\nNew line content!\nLine 2"); + $parserMultiline = new VCardParser($vcardMultiline->buildVCard()); + + $resolve = $parser->getCardAtIndex(0)->getNote(); + $this->assertSame($resolve, 'This is a testnote'); + + $resolve = $parserMultiline->getCardAtIndex(0)->getNote(); + $this->assertSame($resolve, 'This is a multiline note' . \PHP_EOL . 'New line content!' . \PHP_EOL . 'Line 2'); + } + + public function test_it_can_set_and_get_categories_correctly(): void + { + $this->vcard->addCategories([ + 'Category 1', + 'cat-2', + 'another long category!', + ]); + $parser = new VCardParser($this->vcard->buildVCard()); + $resolve = $parser->getCardAtIndex(0)->getCategories(); + $this->assertSame($resolve[0], 'Category 1'); + $this->assertSame($resolve[1], 'cat-2'); + $this->assertSame($resolve[2], 'another long category!'); + } + + public function test_it_can_set_and_get_titlecorrectly(): void + { + $this->vcard->addJobtitle('Ninja'); + $parser = new VCardParser($this->vcard->buildVCard()); + $this->assertSame($parser->getCardAtIndex(0)->getTitle(), 'Ninja'); + } + + public function test_it_can_set_and_get_raw_logo_correctly(): void + { + $image = __DIR__ . '/image.jpg'; + $imageUrl = 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg'; + + $card = new VCard(); + $card->addLogo($image, true); + $parser = new VCardParser($card->buildVCard()); + $this->assertSame($parser->getCardAtIndex(0)->getRawLogo(), file_get_contents($image)); + + $card = new VCard(); + $card->addLogo($image, false); + $parser = new VCardParser($card->buildVCard()); + $this->assertSame($parser->getCardAtIndex(0)->getLogo(), __DIR__ . '/image.jpg'); + + $card = new VCard(); + $card->addLogo($imageUrl, false); + $parser = new VCardParser($card->buildVCard()); + $this->assertSame($parser->getCardAtIndex(0)->getLogo(), $imageUrl); + } + + public function test_it_can_set_and_get_raw_photo_correctly(): void + { + $image = __DIR__ . '/image.jpg'; + $imageUrl = 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg'; + + $card = new VCard(); + $card->addPhoto($image, true); + $parser = new VCardParser($card->buildVCard()); + $this->assertSame($parser->getCardAtIndex(0)->getRawPhoto(), file_get_contents($image)); + + $card = new VCard(); + $card->addPhoto($image, false); + $parser = new VCardParser($card->buildVCard()); + $this->assertSame($parser->getCardAtIndex(0)->getPhoto(), __DIR__ . '/image.jpg'); + + $card = new VCard(); + $card->addPhoto($imageUrl, false); + $parser = new VCardParser($card->buildVCard()); + $this->assertSame($parser->getCardAtIndex(0)->getPhoto(), $imageUrl); + } + + public function test_it_can_handle_multiple_vcards_on_input_correctly(): void + { + $db = ''; + $card = new VCard(); + $card->addName('Desloovere', 'Jeroen'); + $db .= $card->buildVCard(); + + $card2 = new VCard(); + $card2->addName('Lorem', 'Ipsum'); + $db .= $card2->buildVCard(); + + $parser = new VCardParser($db); + $this->assertSame($parser->getCardAtIndex(0)->getName(), 'Jeroen Desloovere'); + $this->assertSame($parser->getCardAtIndex(1)->getName(), 'Ipsum Lorem'); + } + + public function test_it_can_iterate_over_multiple_cards_correctly(): void + { + $db = ''; + + $card = new VCard(); + $card->addName('Desloovere', 'Jeroen'); + $db .= $card->buildVCard(); + + $card2 = new VCard(); + $card2->addName('Lorem', 'Ipsum'); + $db .= $card2->buildVCard(); + + $parser = new VCardParser($db); + foreach ($parser as $i => $card) { + $this->assertInstanceOf(CardData::class, $card); + $this->assertSame( + $card->getName(), + $i === 0 + ? 'Jeroen Desloovere' + : 'Ipsum Lorem', + ); + } + } + + public function test_it_can_load_vcard_from_file(): void + { + $parser = VCardParser::parseFromFile(__DIR__ . '/example.vcf'); + $cards = $parser->getCards(); + foreach ($cards as $card) { + $this->assertInstanceOf(CardData::class, $card); + } + + $this->assertSame($cards[0]->getFirstName(), 'Jeroen'); + $this->assertSame($cards[0]->getLastName(), 'Desloovere'); + $this->assertSame($cards[0]->getName(), 'Jeroen Desloovere'); + $this->assertSame($cards[0]->getUrls()['default'][0], 'http://www.jeroendesloovere.be'); + $this->assertSame($cards[0]->getEmails()['INTERNET'][0], 'site@example.com'); + } + + public function test_it_will_throw_an_exception_when_file_cannot_be_loaded(): void + { + $this->expectException(InvalidArgumentException::class); + VCardParser::parseFromFile(__DIR__ . '/does-not-exist.vcf'); + } + + public function test_it_can_correctly_return_a_label(): void + { + $label = 'street, worktown, workpostcode Belgium'; + + $this->vcard->addLabel($label, 'work'); + $parser = new VCardParser($this->vcard->buildVCard()); + $this->assertSame($parser->getCardAtIndex(0)->getLabel(), $label); + } +} diff --git a/Tests/VCardTest.php b/Tests/VCardTest.php new file mode 100644 index 0000000..6c1ddb4 --- /dev/null +++ b/Tests/VCardTest.php @@ -0,0 +1,243 @@ + 'john@work.com']], + [['WORK' => 'john@work.com', 'HOME' => 'john@home.com']], + [['PREF;WORK' => 'john@work.com', 'HOME' => 'john@home.com']], + ]; + } + + protected function setUp(): void + { + date_default_timezone_set('Europe/Berlin'); + $this->vcard = new VCard(); + $this->firstName = 'Jeroen'; + $this->lastName = 'Desloovere'; + $this->additional = '&'; + $this->prefix = 'Mister'; + $this->suffix = 'Junior'; + $this->emailAddress1 = ''; + $this->emailAddress2 = ''; + $this->firstName2 = 'Ali'; + $this->lastName2 = 'ÖZSÜT'; + $this->firstName3 = 'Garçon'; + $this->lastName3 = 'Jéroèn'; + + $this->vcard->addEmail($this->emailAddress1); + $this->vcard->addEmail($this->emailAddress2); + + $this->vcard->addAddress( + '', + '88th Floor', + '555 East Flours Street', + 'Los Angeles', + 'CA', + '55555', + 'USA', + ); + } + + public function test_it_can_add_an_address(): void + { + $output = $this->vcard->getOutput(); + $this->assertStringContainsString( + 'ADR;WORK;POSTAL;CHARSET=utf-8:;88th Floor;555 East Flours Street;Los Angele', + $output, + ); + + $this->assertStringNotContainsString( + 'ADR;WORK;POSTAL;CHARSET=utf-8:;88th Floor;555 East Flours Street;Los Angeles;CA;55555;', + $output, + ); + } + + public function test_it_cannot_add_a_remote_text_file_as_photo(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Returned data is not an image.'); + $this->vcard->addPhoto('https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/empty.jpg'); + } + + public function test_it_cannot_add_an_empty_picture_as_photo(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Returned data is not an image.'); + $this->vcard->addPhotoContent(''); + } + + public function test_it_cannot_add_a_remote_text_file_as_logo(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Returned data is not an image.'); + $this->vcard->addLogoContent(''); + } + + public function test_it_cannot_add_an_empty_picture_as_logo(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Returned data is not an image.'); + $this->vcard->addPhoto(__DIR__ . '/emptyfile'); + } + + public function test_it_cannot_add_a_empty_photo_as_logo(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Returned data is not an image.'); + $this->vcard->addLogo(__DIR__ . '/emptyfile'); + } + + public function test_it_cannot_add_a_empty_photo_as_photo(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Returned data is not an image.'); + $this->vcard->addPhoto(__DIR__ . '/wrongfile', true); + } + + public function test_it_cannot_add_empty_image_as_logo(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Returned data is not an image.'); + $this->vcard->addLogo(__DIR__ . '/wrongfile'); + } + + public function test_it_will_correctly_return_the_charset(): void + { + $charset = 'ISO-8859-1'; + $this->vcard->setCharset($charset); + $this->assertSame($charset, $this->vcard->getCharset()); + } + + /** @dataProvider emailDataProvider */ + public function test_it_can_set_emails_as_expected(array $emails): void + { + foreach ($emails as $key => $email) { + $this->vcard->addEmail( + $email, + is_string($key) + ? explode(';', $key) + : [], + ); + } + + $output = $this->vcard->getOutput(); + foreach ($emails as $key => $email) { + if (is_string($key)) { + $this->assertStringContainsString(sprintf('EMAIL;INTERNET;%s:%s', $key, $email), $output); + } else { + $this->assertStringContainsString(sprintf('EMAIL;INTERNET:%s', $email), $output); + } + } + } + + public function test_it_can_set_and_transform_names_correctly(): void + { + $this->vcard->addName( + $this->lastName, + $this->firstName, + ); + + $this->assertEquals('jeroen-desloovere', $this->vcard->getFilename()); + } + + public function test_it_can_correctly_evaluate_full_name(): void + { + $this->vcard->addName( + $this->lastName, + $this->firstName, + $this->additional, + $this->prefix, + $this->suffix, + ); + + $this->assertEquals('mister-jeroen-desloovere-junior', $this->vcard->getFilename()); + } + + public function test_it_can_evaluate_special_characters_properly(): void + { + $this->vcard->addName( + $this->lastName2, + $this->firstName2, + ); + + $this->assertEquals('ali-ozsut', $this->vcard->getFilename()); + } + + public function test_it_can_evaluate_special_characters_properly_second(): void + { + $this->vcard->addName( + $this->lastName3, + $this->firstName3, + ); + + $this->assertEquals('garcon-jeroen', $this->vcard->getFilename()); + } + + public function test_property_count_and_contents(): void + { + $this->assertCount(3, $this->vcard->getProperties()); + $this->vcard->addLabel('My label'); + $this->vcard->addLabel('My work label', 'WORK'); + + $resolve = $this->vcard->getOutput(); + $this->assertStringContainsString('LABEL;CHARSET=utf-8:My label', $resolve); + $this->assertStringContainsString('LABEL;WORK;CHARSET=utf-8:My work label', $resolve); + } + + public function test_it_can_correctly_invoke_ChunkSplitUnicode(): void + { + $class_handler = new \ReflectionClass('JeroenDesloovere\VCard\VCard'); + $method_handler = $class_handler->getMethod('chunkSplitUnicode'); + $method_handler->setAccessible(true); + + $ascii_input = 'Lorem ipsum dolor sit amet,'; + $ascii_output = $method_handler->invokeArgs(new VCard(), [$ascii_input, 10, '|']); + $unicode_input = 'Τη γλώσσα μου έδωσαν ελληνική το σπίτι φτωχικό στις αμμουδιές του Ομήρου.'; + $unicode_output = $method_handler->invokeArgs(new VCard(), [$unicode_input, 10, '|']); + + $this->assertEquals( + 'Lorem ipsu|m dolor si|t amet,|', + $ascii_output, + ); + $this->assertEquals( + 'Τη γλώσσα |μου έδωσαν| ελληνική |το σπίτι φ|τωχικό στι|ς αμμουδιέ|ς του Ομήρ|ου.|', + $unicode_output, + ); + } +} diff --git a/tests/empty.jpg b/Tests/empty.jpg similarity index 100% rename from tests/empty.jpg rename to Tests/empty.jpg diff --git a/tests/emptyfile b/Tests/emptyfile similarity index 100% rename from tests/emptyfile rename to Tests/emptyfile diff --git a/tests/example.vcf b/Tests/example.vcf similarity index 100% rename from tests/example.vcf rename to Tests/example.vcf diff --git a/tests/image.jpg b/Tests/image.jpg similarity index 100% rename from tests/image.jpg rename to Tests/image.jpg diff --git a/tests/wrongfile b/Tests/wrongfile similarity index 100% rename from tests/wrongfile rename to Tests/wrongfile diff --git a/build/php/Dockerfile b/build/php/Dockerfile new file mode 100644 index 0000000..9584750 --- /dev/null +++ b/build/php/Dockerfile @@ -0,0 +1,17 @@ +FROM php:8.2-cli + +ENV PHP_IDE_CONFIG="serverName=vcard" +RUN pecl install xdebug \ + && docker-php-ext-enable xdebug + +ADD ./etc/php.ini /usr/local/etc/php/php.ini +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +RUN apt-get update && apt-get install -y \ + git \ + unzip \ + libzip-dev \ + && docker-php-ext-install zip + +WORKDIR /var/www/html +CMD ["tail", "-f", "/dev/stdout"] diff --git a/build/php/etc/php.ini b/build/php/etc/php.ini new file mode 100644 index 0000000..5ca326f --- /dev/null +++ b/build/php/etc/php.ini @@ -0,0 +1,25 @@ +magic_quotes_gpc = Off; +register_globals = Off; +file_uploads = On; +default_charset = UTF-8; +memory_limit = 4G; +max_execution_time = 36000; +upload_max_filesize = 999M; +post_max_size = 999M; +safe_mode = Off; +mysql.connect_timeout = 20; +allow_url_fopen = true; +display_errors = 1; +error_reporting = E_ALL; +date.timezone = "Europe/Berlin" + +xdebug.idekey=PHPSTORM +xdebug.max_nesting_level = 2048 +xdebug.mode=debug +xdebug.client_port=9000 +xdebug.client_host=host.docker.internal +xdebug.start_with_request=yes +xdebug.discover_client_host=0 +xdebug.show_error_trace=1 + +pm.max_children = 25 diff --git a/composer.json b/composer.json index f4c8660..13dc5a6 100644 --- a/composer.json +++ b/composer.json @@ -11,19 +11,32 @@ "email": "info@jeroendesloovere.be", "homepage": "http://jeroendesloovere.be", "role": "Developer" + }, + { + "name": "Maximilian Graf Schimmelmann", + "email": "foss@schimmelmann.org", + "homepage": "https://www.schimmelmann.org", + "role": "Developer" } ], "require": { - "php": ">=7.3.0", - "behat/transliterator": "~1.0" + "php": "^8.2", + "behat/transliterator": "~1.0", + "webmozart/assert": "^1.11", + "ext-fileinfo": "*", + "ext-curl": "*" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10", + "symplify/easy-coding-standard": "^12.1", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-webmozart-assert": "^1.2", + "seec/phpunit-consecutive-params": "^1.1" }, "autoload": { - "psr-4": { "JeroenDesloovere\\VCard\\": "src/" } + "psr-4": { "JeroenDesloovere\\VCard\\": "src/"} }, "autoload-dev": { - "psr-4": { "JeroenDesloovere\\VCard\\": "tests/" } + "psr-4": { "JeroenDesloovere\\VCard\\Tests\\": "Tests/"} } } diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..a0b4674 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,7 @@ +version: '3.9' + +services: + php: + build: ./build/php + volumes: + - .:/var/www/html diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..1bc020c --- /dev/null +++ b/ecs.php @@ -0,0 +1,276 @@ +parallel(); + $ecsConfig->sets([SetList::PSR_12]); + $ecsConfig->rule(CastSpacesFixer::class); + $ecsConfig->rule(ClassAttributesSeparationFixer::class); + $ecsConfig->rule(EncodingFixer::class); + $ecsConfig->rule(EregToPregFixer::class); + $ecsConfig->rule(LowercaseCastFixer::class); + $ecsConfig->rule(LowerCaseConstantSniff::class); + $ecsConfig->rule(LowercaseKeywordsFixer::class); + $ecsConfig->rule(LowercaseStaticReferenceFixer::class); + $ecsConfig->rule(MagicConstantCasingFixer::class); + $ecsConfig->rule(ModernizeTypesCastingFixer::class); + $ecsConfig->rule(NativeFunctionCasingFixer::class); + $ecsConfig->rule(NoAliasFunctionsFixer::class); + $ecsConfig->rule(NoBlankLinesAfterClassOpeningFixer::class); + $ecsConfig->rule(NoMultilineWhitespaceAroundDoubleArrowFixer::class); + $ecsConfig->rule(NonPrintableCharacterFixer::class); + $ecsConfig->rule(NoNullPropertyInitializationFixer::class); + $ecsConfig->rule(NoPhp4ConstructorFixer::class); + $ecsConfig->rule(NormalizeIndexBraceFixer::class); + $ecsConfig->rule(NoShortBoolCastFixer::class); + $ecsConfig->rule(NoUnneededFinalMethodFixer::class); + $ecsConfig->rule(NoWhitespaceBeforeCommaInArrayFixer::class); + $ecsConfig->rule(PowToExponentiationFixer::class); + $ecsConfig->rule(ProtectedToPrivateFixer::class); + $ecsConfig->rule(SelfAccessorFixer::class); + $ecsConfig->rule(ShortScalarCastFixer::class); + $ecsConfig->rule(SingleClassElementPerStatementFixer::class); + $ecsConfig->rule(TrimArraySpacesFixer::class); + $ecsConfig->rule(WhitespaceAfterCommaInArrayFixer::class); + $ecsConfig->rule(NoEmptyCommentFixer::class); + $ecsConfig->rule(NoTrailingWhitespaceInCommentFixer::class); + $ecsConfig->rule(CombineConsecutiveIssetsFixer::class); + $ecsConfig->rule(CombineConsecutiveUnsetsFixer::class); + $ecsConfig->rule(DeclareEqualNormalizeFixer::class); + $ecsConfig->rule(DirConstantFixer::class); + $ecsConfig->rule(ElseifFixer::class); + $ecsConfig->rule(FunctionDeclarationFixer::class); + $ecsConfig->rule(FunctionToConstantFixer::class); + $ecsConfig->rule(IncludeFixer::class); + $ecsConfig->rule(IsNullFixer::class); + $ecsConfig->rule(MethodArgumentSpaceFixer::class); + $ecsConfig->rule(NativeConstantInvocationFixer::class); + $ecsConfig->rule(NoBreakCommentFixer::class); + $ecsConfig->rule(NoLeadingImportSlashFixer::class); + $ecsConfig->rule(NoSpacesAfterFunctionNameFixer::class); + $ecsConfig->rule(NoSuperfluousElseifFixer::class); + $ecsConfig->rule(NoUnneededControlParenthesesFixer::class); + $ecsConfig->rule(NoUnneededCurlyBracesFixer::class); + $ecsConfig->rule(NoUnusedImportsFixer::class); + $ecsConfig->rule(NoUselessElseFixer::class); + $ecsConfig->rule(OrderedImportsFixer::class); + $ecsConfig->rule(ReturnTypeDeclarationFixer::class); + $ecsConfig->rule(SingleImportPerStatementFixer::class); + $ecsConfig->rule(SingleLineAfterImportsFixer::class); + $ecsConfig->rule(SwitchCaseSemicolonToColonFixer::class); + $ecsConfig->rule(SwitchCaseSpaceFixer::class); + $ecsConfig->rule(BinaryOperatorSpacesFixer::class); + $ecsConfig->rule(BlankLineAfterNamespaceFixer::class); + $ecsConfig->rule(NoHomoglyphNamesFixer::class); + $ecsConfig->rule(NoLeadingNamespaceWhitespaceFixer::class); + $ecsConfig->rule(BlankLineAfterOpeningTagFixer::class); + $ecsConfig->rule(BlankLineBeforeStatementFixer::class); + $ecsConfig->rule(DeclareStrictTypesFixer::class); + $ecsConfig->rule(FullOpeningTagFixer::class); + $ecsConfig->rule(IndentationTypeFixer::class); + $ecsConfig->rule(LineEndingFixer::class); + $ecsConfig->rule(NewWithBracesFixer::class); + $ecsConfig->rule(NoBlankLinesAfterPhpdocFixer::class); + $ecsConfig->rule(NoClosingTagFixer::class); + $ecsConfig->rule(NoEmptyPhpdocFixer::class); + $ecsConfig->rule(NoEmptyStatementFixer::class); + $ecsConfig->rule(NoSinglelineWhitespaceBeforeSemicolonsFixer::class); + $ecsConfig->rule(NoSpacesAroundOffsetFixer::class); + $ecsConfig->rule(NoSpacesInsideParenthesisFixer::class); + $ecsConfig->rule(NoTrailingWhitespaceFixer::class); + $ecsConfig->rule(NoWhitespaceInBlankLineFixer::class); + $ecsConfig->rule(ObjectOperatorWithoutWhitespaceFixer::class); + $ecsConfig->rule(PhpdocIndentFixer::class); + $ecsConfig->rule(PhpdocNoAccessFixer::class); + $ecsConfig->rule(PhpdocNoAliasTagFixer::class); + $ecsConfig->rule(PhpdocNoEmptyReturnFixer::class); + $ecsConfig->rule(PhpdocNoPackageFixer::class); + $ecsConfig->rule(PhpdocNoUselessInheritdocFixer::class); + $ecsConfig->rule(PhpdocReturnSelfReferenceFixer::class); + $ecsConfig->rule(PhpdocScalarFixer::class); + $ecsConfig->rule(PhpdocSeparationFixer::class); + $ecsConfig->rule(PhpdocSingleLineVarSpacingFixer::class); + $ecsConfig->rule(PhpdocTrimFixer::class); + $ecsConfig->rule(PhpdocTypesFixer::class); + $ecsConfig->rule(PhpdocVarWithoutNameFixer::class); + $ecsConfig->rule(PhpUnitDedicateAssertFixer::class); + $ecsConfig->rule(PhpUnitFqcnAnnotationFixer::class); + $ecsConfig->rule(SingleBlankLineAtEofFixer::class); + $ecsConfig->rule(SingleQuoteFixer::class); + $ecsConfig->rule(SpaceAfterSemicolonFixer::class); + $ecsConfig->rule(StandardizeNotEqualsFixer::class); + $ecsConfig->rule(TernaryOperatorSpacesFixer::class); + $ecsConfig->rule(TernaryToNullCoalescingFixer::class); + $ecsConfig->rule(UnaryOperatorSpacesFixer::class); + $ecsConfig->ruleWithConfiguration(TrailingCommaInMultilineFixer::class, [ + 'elements' => [ + 'arguments', + 'parameters', + ], + ]); + $ecsConfig->ruleWithConfiguration(NoMixedEchoPrintFixer::class, ['use' => 'echo']); + $ecsConfig->ruleWithConfiguration(ArraySyntaxFixer::class, ['syntax' => 'short']); + $ecsConfig->ruleWithConfiguration(ClassDefinitionFixer::class, [ + 'single_item_single_line' => true, + 'multi_line_extends_each_single_line' => true, + ]); + $ecsConfig->ruleWithConfiguration(VisibilityRequiredFixer::class, [ + 'elements' => [ + 'const', + 'property', + 'method', + ], + ]); + $ecsConfig->ruleWithConfiguration(SingleLineCommentStyleFixer::class, [ + 'comment_types' => [ + 'hash', + ] + ]); + $ecsConfig->ruleWithConfiguration(ListSyntaxFixer::class, [ + 'syntax' => 'short', + ]); + $ecsConfig->ruleWithConfiguration(ConcatSpaceFixer::class, [ + 'spacing' => 'one', + ]); + $ecsConfig->ruleWithConfiguration(IncrementStyleFixer::class, [ + 'style' => 'pre' + ]); + $ecsConfig->ruleWithConfiguration(NoExtraBlankLinesFixer::class, [ + 'tokens' => [ + 'break', + 'case', + 'continue', + 'curly_brace_block', + 'default', + 'extra', + 'parenthesis_brace_block', + 'return', + 'square_brace_block', + 'switch', + 'throw', + 'use', + ], + ]); + $ecsConfig->ruleWithConfiguration(PhpdocTypesOrderFixer::class, [ + 'null_adjustment' => 'always_last', + 'sort_algorithm' => 'none', + ]); + $ecsConfig->ruleWithConfiguration(NoSuperfluousPhpdocTagsFixer::class, [ + 'allow_mixed' => true, + ]); +}; diff --git a/examples/example.php b/examples/example.php index b9264f7..cb4334d 100644 --- a/examples/example.php +++ b/examples/example.php @@ -5,7 +5,7 @@ */ require_once __DIR__ . '/../vendor/autoload.php'; -require_once __DIR__ . '/../src/VCard.php'; +require_once __DIR__.'/../src_/VCard.php'; use JeroenDesloovere\VCard\VCard; @@ -39,10 +39,9 @@ //return $vcard->getOutput(); // return vcard as a download -return $vcard->download(); +print $vcard->download(); -// echo message -echo 'A personal vCard is saved in this folder: ' . __DIR__; +print 'A personal vCard is saved in this folder: ' . __DIR__; // or diff --git a/examples/example_parsing.php b/examples/example_parsing.php index 58f5c4a..91fccd1 100644 --- a/examples/example_parsing.php +++ b/examples/example_parsing.php @@ -1,13 +1,7 @@ lastname; $firstname = $vcard->firstname; - $birthday = $vcard->birthday->format('Y-m-d'); + $birthday = $vcard->getBirthday()->format('Y-m-d'); printf("\"%s\",\"%s\",\"%s\"", $lastname, $firstname, $birthday); diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..5d69995 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + reportUnmatchedIgnoredErrors: false + checkMissingIterableValueType: false + checkGenericClassInNonGenericObjectType: false + parallel: + processTimeout: 300.0 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a61d486..ebf6dd2 100755 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,22 @@ - - - - src - - + + tests + + + + src + + + + + + src + + + diff --git a/src/Dto/AddressData.php b/src/Dto/AddressData.php new file mode 100644 index 0000000..a1e948d --- /dev/null +++ b/src/Dto/AddressData.php @@ -0,0 +1,67 @@ +name; + } + + public function getExtended(): string + { + return $this->extended; + } + + public function getStreet(): string + { + return $this->street; + } + + public function getCity(): string + { + return $this->city; + } + + public function getRegion(): string + { + return $this->region; + } + + public function getZip(): string + { + return $this->zip; + } + + public function getCountry(): string + { + return $this->country; + } + + public function toArray(): array + { + return [ + 'name' => $this->name, + 'extended' => $this->extended, + 'street' => $this->street, + 'city' => $this->city, + 'region' => $this->region, + 'zip' => $this->zip, + 'country' => $this->country, + ]; + } +} diff --git a/src/Dto/CardData.php b/src/Dto/CardData.php new file mode 100644 index 0000000..657bb64 --- /dev/null +++ b/src/Dto/CardData.php @@ -0,0 +1,286 @@ +name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getPhone(): array + { + return $this->phone; + } + + public function addPhone(string $key, string $phone): void + { + $this->phone[$key][] = $phone; + } + + public function getEmails(): array + { + return $this->email; + } + + public function addEmail(string $key, string $email): void + { + $this->email[$key][] = $email; + } + + public function getAddress(): array + { + return $this->address; + } + + public function addAddress(string $key, ?AddressData $address): void + { + $this->address[$key][] = $address; + } + + public function getWebsite(): string + { + return $this->website; + } + + public function setWebsite(string $website): void + { + $this->website = $website; + } + + public function getPhoto(): string + { + return $this->photo; + } + + public function setPhoto(string $photo): void + { + $this->photo = $photo; + } + + public function getBirthday(): DateTimeImmutable + { + return $this->birthday; + } + + public function setBirthday(DateTimeImmutable $birthday): void + { + $this->birthday = $birthday; + } + + public function getUrls(): array + { + return $this->url; + } + + public function addUrl(string $key, string $url): void + { + $this->url[$key][] = $url; + } + + public function getLogo(): string + { + return $this->logo; + } + + public function setLogo(string $logo): void + { + $this->logo = $logo; + } + + public function getRawPhoto(): string + { + return $this->rawPhoto; + } + + public function setRawPhoto(string $rawPhoto): void + { + $this->rawPhoto = $rawPhoto; + } + + public function getRawLogo(): string + { + return $this->rawLogo; + } + + public function setRawLogo(string $rawLogo): void + { + $this->rawLogo = $rawLogo; + } + + public function getCategories(): array + { + return $this->categories; + } + + public function setCategories(array $categories): void + { + $this->categories = $categories; + } + + public function getLabel(): string + { + return $this->label; + } + + public function setLabel(string $label): void + { + $this->label = $label; + } + + public function getRevision(): string + { + return $this->revision; + } + + public function setRevision(string $revision): void + { + $this->revision = $revision; + } + + public function getVersion(): string + { + return $this->version; + } + + public function setVersion(string $version): void + { + $this->version = $version; + } + + public function getOrganization(): string + { + return $this->organization; + } + + public function setOrganization(string $organization): void + { + $this->organization = $organization; + } + + public function getNote(): string + { + return $this->note; + } + + public function setNote(string $note): void + { + $this->note = $note; + } + + public function getFirstName(): string + { + return $this->firstName; + } + + public function setFirstName(string $firstName): void + { + $this->firstName = $firstName; + } + + public function getLastName(): string + { + return $this->lastName; + } + + public function setLastName(string $lastName): void + { + $this->lastName = $lastName; + } + + public function getAdditional(): string + { + return $this->additional; + } + + public function setAdditional(string $additional): void + { + $this->additional = $additional; + } + + public function getPrefix(): string + { + return $this->prefix; + } + + public function setPrefix(string $prefix): void + { + $this->prefix = $prefix; + } + + public function getSuffix(): string + { + return $this->suffix; + } + + public function setSuffix(string $suffix): void + { + $this->suffix = $suffix; + } +} diff --git a/src/VCard.php b/src/VCard.php index 8ccc6c4..2199671 100644 --- a/src/VCard.php +++ b/src/VCard.php @@ -1,254 +1,165 @@ setProperty( 'address', - 'ADR' . (($type != '') ? ';' . $type : '') . $this->getCharsetString(), - $value + sprintf( + 'ADR%s%s', + $type !== [] ? ';' . implode(';', $type) : '', + $this->getCharsetString(), + ), + sprintf( + '%s;%s;%s;%s;%s;%s;%s', + $name, + $extended, + $street, + $city, + $region, + $zip, + $country, + ), ); - - return $this; } - /** - * Add birthday - * - * @param string $date Format is YYYY-MM-DD - * @return $this - */ - public function addBirthday($date) + public function addBirthday(DateTimeImmutable $date): void { $this->setProperty( 'birthday', 'BDAY', - $date + $date->format('Y-m-d'), ); - - return $this; } - /** - * Add company - * - * @param string $company - * @param string $department - * @return $this - */ - public function addCompany($company, $department = '') + public function addCompany(string $company, string $department = ''): void { $this->setProperty( 'company', 'ORG' . $this->getCharsetString(), $company - . ($department != '' ? ';' . $department : '') + . ($department !== '' ? ';' . $department : ''), ); - // if filename is empty, add to filename if ($this->filename === null) { $this->setFilename($company); } - - return $this; } - /** - * Add email - * - * @param string $address The e-mail address - * @param string [optional] $type The type of the email address - * $type may be PREF | WORK | HOME - * or any combination of these: e.g. "PREF;WORK" - * @return $this - */ - public function addEmail($address, $type = '') + public function addEmail(string $address, array $type = []): void { + Assert::allInArray($type, ['PREF', 'WORK', 'HOME']); + $this->setProperty( 'email', - 'EMAIL;INTERNET' . (($type != '') ? ';' . $type : ''), - $address + 'EMAIL;INTERNET' . (($type !== []) ? ';' . implode(';', $type) : ''), + $address, ); - - return $this; } - /** - * Add jobtitle - * - * @param string $jobtitle The jobtitle for the person. - * @return $this - */ - public function addJobtitle($jobtitle) + public function addJobtitle(string $jobtitle): void { $this->setProperty( 'jobtitle', 'TITLE' . $this->getCharsetString(), - $jobtitle + $jobtitle, ); - - return $this; } - /** - * Add a label - * - * @param string $label - * @param string $type - * - * @return $this - */ - public function addLabel($label, $type = '') + public function addLabel(string $label, string $type = ''): void { $this->setProperty( 'label', - 'LABEL' . ($type !== '' ? ';' . $type : '') . $this->getCharsetString(), - $label + 'LABEL' . ($type === '' ? '' : ';' . $type) . $this->getCharsetString(), + $label, ); - - return $this; } - /** - * Add role - * - * @param string $role The role for the person. - * @return $this - */ - public function addRole($role) + public function addRole(string $role): void { $this->setProperty( 'role', 'ROLE' . $this->getCharsetString(), - $role + $role, ); - - return $this; } - /** - * Add a photo or logo (depending on property name) - * - * @param string $property LOGO|PHOTO - * @param string $url image url or filename - * @param bool $include Do we include the image in our vcard or not? - * @param string $element The name of the element to set - * @throws VCardException - */ - private function addMedia($property, $url, $element, $include = true) - { + private function addMedia( + string $property, + string $url, + string $element, + bool $include = true, + ): void { $mimeType = null; - //Is this URL for a remote resource? if (filter_var($url, FILTER_VALIDATE_URL) !== false) { - $headers = get_headers($url, 1); - - if (array_key_exists('Content-Type', $headers)) { + $headers = get_headers($url, true); + if ($headers !== false && array_key_exists('Content-Type', $headers)) { $mimeType = $headers['Content-Type']; if (is_array($mimeType)) { $mimeType = end($mimeType); } } } else { - //Local file, so inspect it directly $mimeType = mime_content_type($url); } - if (strpos($mimeType, ';') !== false) { + + if (str_contains($mimeType, ';')) { $mimeType = strstr($mimeType, ';', true); } - if (!is_string($mimeType) || substr($mimeType, 0, 6) !== 'image/') { + + if (!is_string($mimeType) || !str_starts_with($mimeType, 'image/')) { throw VCardException::invalidImage(); } + $fileType = strtoupper(substr($mimeType, 6)); if ($include) { @@ -266,75 +177,63 @@ private function addMedia($property, $url, $element, $include = true) throw VCardException::emptyURL(); } + Assert::string($value); $value = base64_encode($value); - $property .= ";ENCODING=b;TYPE=" . $fileType; + $property .= ';ENCODING=b;TYPE=' . $fileType; } else { if (filter_var($url, FILTER_VALIDATE_URL) !== false) { $propertySuffix = ';VALUE=URL'; $propertySuffix .= ';TYPE=' . strtoupper($fileType); $property = $property . $propertySuffix; - $value = $url; - } else { - $value = $url; } + $value = $url; } $this->setProperty( $element, $property, - $value + $value, ); } - /** - * Add a photo or logo (depending on property name) - * - * @param string $property LOGO|PHOTO - * @param string $content image content - * @param string $element The name of the element to set - */ - private function addMediaContent($property, $content, $element) - { - $finfo = new \finfo(); + private function addMediaContent( + string $property, + string $content, + string $element, + ): void { + $finfo = new finfo(); $mimeType = $finfo->buffer($content, FILEINFO_MIME_TYPE); - if (strpos($mimeType, ';') !== false) { + Assert::string($mimeType); + if (str_contains($mimeType, ';') === true) { $mimeType = strstr($mimeType, ';', true); + Assert::string($mimeType); } - if (!is_string($mimeType) || substr($mimeType, 0, 6) !== 'image/') { + + if (str_starts_with($mimeType, 'image/') === false) { throw VCardException::invalidImage(); } + $fileType = strtoupper(substr($mimeType, 6)); $content = base64_encode($content); - $property .= ";ENCODING=b;TYPE=" . $fileType; + $property .= ';ENCODING=b;TYPE=' . $fileType; $this->setProperty( $element, $property, - $content + $content, ); } - /** - * Add name - * - * @param string [optional] $lastName - * @param string [optional] $firstName - * @param string [optional] $additional - * @param string [optional] $prefix - * @param string [optional] $suffix - * @return $this - */ public function addName( - $lastName = '', - $firstName = '', - $additional = '', - $prefix = '', - $suffix = '' - ) { - // define values with non-empty values + string $lastName = '', + string $firstName = '', + string $additional = '', + string $prefix = '', + string $suffix = '', + ): void { $values = array_filter([ $prefix, $firstName, @@ -342,416 +241,233 @@ public function addName( $lastName, $suffix, ]); - - // define filename $this->setFilename($values); - // set property - $property = $lastName . ';' . $firstName . ';' . $additional . ';' . $prefix . ';' . $suffix; $this->setProperty( 'name', 'N' . $this->getCharsetString(), - $property + sprintf('%s;%s;%s;%s;%s', $lastName, $firstName, $additional, $prefix, $suffix), ); - // is property FN set? - if (!$this->hasProperty('FN')) { - // set property + if ($this->hasProperty('FN') === false) { $this->setProperty( 'fullname', 'FN' . $this->getCharsetString(), - trim(implode(' ', $values)) + trim(implode(' ', $values)), ); } - - return $this; } - /** - * Add note - * - * @param string $note - * @return $this - */ - public function addNote($note) + public function addNote(string $note): void { $this->setProperty( 'note', 'NOTE' . $this->getCharsetString(), - $note + $note, ); - - return $this; } - /** - * Add categories - * - * @param array $categories - * @return $this - */ - public function addCategories($categories) + public function addCategories(array $categories): void { $this->setProperty( 'categories', 'CATEGORIES' . $this->getCharsetString(), - trim(implode(',', $categories)) + trim(implode(',', $categories)), ); - - return $this; } - /** - * Add phone number - * - * @param string $number - * @param string [optional] $type - * Type may be PREF | WORK | HOME | VOICE | FAX | MSG | - * CELL | PAGER | BBS | CAR | MODEM | ISDN | VIDEO - * or any senseful combination, e.g. "PREF;WORK;VOICE" - * @return $this - */ - public function addPhoneNumber($number, $type = '') + public function addPhoneNumber(string $number, array $type = []): void { + Assert::allInArray( + $type, + ['PREF', 'WORK', 'HOME', 'VOICE', 'FAX', 'MSG', 'CELL', 'PAGER', 'BBS', 'CAR', 'MODEM', 'ISDN', 'VIDEO'], + ); + $this->setProperty( 'phoneNumber', - 'TEL' . (($type != '') ? ';' . $type : ''), - $number + 'TEL' . (($type != '') ? ';' . implode(';', $type) : ''), + $number, ); - - return $this; } - /** - * Add Logo - * - * @param string $url image url or filename - * @param bool $include Include the image in our vcard? - * @return $this - */ - public function addLogo($url, $include = true) + public function addLogo(string $url, bool $include = true): void { $this->addMedia( 'LOGO', $url, 'logo', - $include + $include, ); - - return $this; } - /** - * Add Logo content - * - * @param string $content image content - * @return $this - */ - public function addLogoContent($content) + public function addLogoContent(string $content): void { $this->addMediaContent( 'LOGO', $content, - 'logo' + 'logo', ); - - return $this; } - /** - * Add Photo - * - * @param string $url image url or filename - * @param bool $include Include the image in our vcard? - * @return $this - */ - public function addPhoto($url, $include = true) + public function addPhoto(string $url, bool $include = true): void { $this->addMedia( 'PHOTO', $url, 'photo', - $include + $include, ); - - return $this; } - /** - * Add Photo content - * - * @param string $content image content - * @return $this - */ - public function addPhotoContent($content) + public function addPhotoContent(string $content): void { $this->addMediaContent( 'PHOTO', $content, - 'photo' + 'photo', ); - - return $this; } - /** - * Add URL - * - * @param string $url - * @param string [optional] $type Type may be WORK | HOME - * @return $this - */ - public function addURL($url, $type = '') + public function addURL(string $url, string $type = ''): void { $this->setProperty( 'url', 'URL' . (($type != '') ? ';' . $type : ''), - $url + $url, ); - - return $this; } - /** - * Build VCard (.vcf) - * - * @return string - */ - public function buildVCard() + public function buildVCard(): string { - // init string - $string = "BEGIN:VCARD\r\n"; - $string .= "VERSION:3.0\r\n"; - $string .= "REV:" . date("Y-m-d") . "T" . date("H:i:s") . "Z\r\n"; + $string = VCardInterface::VCARD_START . \PHP_EOL; + $string .= sprintf('%s:%s%s', self::VCARD_VERSION, self::VCARD_VERSION_VALUE, \PHP_EOL); + $string .= sprintf('%s%sT%sZ%s', self::VCARD_REVISION, date('Y-m-d'), date('H:i:s'), \PHP_EOL); - // loop all properties $properties = $this->getProperties(); foreach ($properties as $property) { - // add to string $string .= $this->fold($property['key'] . ':' . $this->escape($property['value'])) . "\r\n"; } - // add to string - $string .= "END:VCARD\r\n"; + $string .= self::VCARD_END . \PHP_EOL; - // return return $string; } - /** - * Build VCalender (.ics) - Safari (< iOS 8) can not open .vcf files, so we have build a workaround. - * - * @return string - */ - public function buildVCalendar() + public function buildVCalendar(): string { // init dates - $dtstart = date("Ymd") . "T" . date("Hi") . "00"; - $dtend = date("Ymd") . "T" . date("Hi") . "01"; + $start = date('Ymd') . 'T' . date('Hi') . '00'; + $end = date('Ymd') . 'T' . date('Hi') . '01'; // init string $string = "BEGIN:VCALENDAR\n"; $string .= "VERSION:2.0\n"; $string .= "BEGIN:VEVENT\n"; - $string .= "DTSTART;TZID=Europe/London:" . $dtstart . "\n"; - $string .= "DTEND;TZID=Europe/London:" . $dtend . "\n"; + $string .= 'DTSTART;TZID=Europe/London:' . $start . "\n"; + $string .= 'DTEND;TZID=Europe/London:' . $end . "\n"; $string .= "SUMMARY:Click attached contact below to save to your contacts\n"; - $string .= "DTSTAMP:" . $dtstart . "Z\n"; + $string .= 'DTSTAMP:' . $start . "Z\n"; $string .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/directory;\n"; - $string .= " X-APPLE-FILENAME=" . $this->getFilename() . "." . $this->getFileExtension() . ":\n"; + $string .= ' X-APPLE-FILENAME=' . $this->getFilename() . '.' . $this->getFileExtension() . ":\n"; - // base64 encode it so that it can be used as an attachemnt to the "dummy" calendar appointment $b64vcard = base64_encode($this->buildVCard()); - - // chunk the single long line of b64 text in accordance with RFC2045 - // (and the exact line length determined from the original .ics file exported from Apple calendar - $b64mline = chunk_split($b64vcard, 74, "\n"); - - // need to indent all the lines by 1 space for the iphone (yes really?!!) + $b64mline = chunk_split($b64vcard, 74); $b64final = preg_replace('/(.+)/', ' $1', $b64mline); $string .= $b64final; - - // output the correctly formatted encoded text $string .= "END:VEVENT\n"; $string .= "END:VCALENDAR\n"; - // return return $string; } - /** - * Returns the browser user agent string. - * - * @return string - */ - protected function getUserAgent() + protected function getUserAgent(): string { - if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) { - $browser = strtolower($_SERVER['HTTP_USER_AGENT']); - } else { - $browser = 'unknown'; - } - - return $browser; + return array_key_exists('HTTP_USER_AGENT', $_SERVER) + ? strtolower($_SERVER['HTTP_USER_AGENT']) + : 'unknown'; } - /** - * Decode - * - * @param string $value The value to decode - * @return string decoded - */ - private function decode($value) + private function decode(string $value): string { - // convert cyrlic, greek or other caracters to ASCII characters return Transliterator::transliterate($value); } - /** - * Download a vcard or vcal file to the browser. - */ - public function download() + public function download(): string { - // define output - $output = $this->getOutput(); - foreach ($this->getHeaders(false) as $header) { header($header); } - // echo the output and it will be a download - echo $output; + return $this->getOutput(); } /** - * Fold a line according to RFC2425 section 5.8.1. - * - * @link http://tools.ietf.org/html/rfc2425#section-5.8.1 - * @param string $text - * @return mixed + * @see https://github.com/jeroendesloovere/vcard/issues/153 */ - protected function fold($text) + protected function fold(string $text): string { - if (strlen($text) <= 75) { - return $text; - } - - // The chunk_split_unicode creates a huge memory footprint when used on long strings (EG photos are base64 10MB results in > 1GB memory usage) - // So check if the string is ASCII (7 bit) and if it is use the built in way RE: https://github.com/jeroendesloovere/vcard/issues/153 - if ($this->is_ascii($text)) { - return substr(chunk_split($text, 75, "\r\n "), 0, -3); - } - - // split, wrap and trim trailing separator - return substr($this->chunk_split_unicode($text, 75, "\r\n "), 0, -3); + return match (true) { + strlen($text) <= 75 => $text, + $this->isAscii($text) => substr(chunk_split($text, 75, "\r\n "), 0, -3), + default => substr($this->chunkSplitUnicode($text, 75, "\r\n "), 0, -3), + }; } - - /** - * Determine if string is pure 7bit ascii - * @link https://pageconfig.com/post/how-to-validate-ascii-text-in-php - * - * @param string $string - * @return bool - */ - protected function is_ascii($string = '' ) { + protected function isAscii(string $string = ''): bool + { $num = 0; - while( isset( $string[$num] ) ) { - if( ord( $string[$num] ) & 0x80 ) { + while (isset($string[$num])) { + if (ord($string[$num]) & 0x80) { return false; } - $num++; + ++$num; } + return true; } - /** - * multibyte word chunk split - * @link http://php.net/manual/en/function.chunk-split.php#107711 - * - * @param string $body The string to be chunked. - * @param integer $chunklen The chunk length. - * @param string $end The line ending sequence. - * @return string Chunked string - */ - protected function chunk_split_unicode($body, $chunklen = 76, $end = "\r\n") + protected function chunkSplitUnicode(string $body, int $chunkLen = 76, string $end = "\r\n"): string { - $array = array_chunk( - preg_split("//u", $body, -1, PREG_SPLIT_NO_EMPTY), $chunklen); - $body = ""; + $parts = preg_split('//u', $body, -1, PREG_SPLIT_NO_EMPTY); + Assert::isArray($parts); + $array = array_chunk($parts, max(1, $chunkLen)); + $body = ''; + foreach ($array as $item) { - $body .= join("", $item) . $end; + $body .= implode('', $item) . $end; } + return $body; } - /** - * Escape newline characters according to RFC2425 section 5.8.4. - * - * @link http://tools.ietf.org/html/rfc2425#section-5.8.4 - * @param string $text - * @return string - */ - protected function escape($text) + protected function escape(string $text): string { - if ($text === null) { - return null; - } - - $text = str_replace("\r\n", "\\n", $text); - $text = str_replace("\n", "\\n", $text); - - return $text; + return str_replace(["\n", "\r\n"], '\\n', $text); } - /** - * Get output as string - * @deprecated in the future - * - * @return string - */ - public function get() + public function get(): string { return $this->getOutput(); } - /** - * Get charset - * - * @return string - */ - public function getCharset() + public function getCharset(): string { return $this->charset; } - /** - * Get charset string - * - * @return string - */ - public function getCharsetString() + public function getCharsetString(): string { return ';CHARSET=' . $this->charset; } - /** - * Get content type - * - * @return string - */ - public function getContentType() + public function getContentType(): string { - return ($this->isIOS7()) ? - 'text/x-vcalendar' : 'text/x-vcard'; + return $this->isIOS7() + ? 'text/x-vcalendar' + : 'text/x-vcard'; } - /** - * Get filename - * - * @return string - */ - public function getFilename() + public function getFilename(): string { if (!$this->filename) { return 'unknown'; @@ -760,40 +476,26 @@ public function getFilename() return $this->filename; } - /** - * Get file extension - * - * @return string - */ - public function getFileExtension() + public function getFileExtension(): string { - return ($this->isIOS7()) ? - 'ics' : 'vcf'; + return $this->isIOS7() + ? self::FILE_EXT_ICS + : self::FILE_EXT_VCF; } - /** - * Get headers - * - * @param bool $asAssociative - * @return array - */ - public function getHeaders($asAssociative) + public function getHeaders(bool $asAssociative): array { $contentType = $this->getContentType() . '; charset=' . $this->getCharset(); $contentDisposition = 'attachment; filename=' . $this->getFilename() . '.' . $this->getFileExtension(); $contentLength = mb_strlen($this->getOutput(), '8bit'); $connection = 'close'; - if ((bool)$asAssociative) { - return [ - 'Content-type' => $contentType, - 'Content-Disposition' => $contentDisposition, - 'Content-Length' => $contentLength, - 'Connection' => $connection, - ]; - } - - return [ + return $asAssociative ? [ + 'Content-type' => $contentType, + 'Content-Disposition' => $contentDisposition, + 'Content-Length' => $contentLength, + 'Connection' => $connection, + ] : [ 'Content-type: ' . $contentType, 'Content-Disposition: ' . $contentDisposition, 'Content-Length: ' . $contentLength, @@ -801,38 +503,19 @@ public function getHeaders($asAssociative) ]; } - /** - * Get output as string - * iOS devices (and safari < iOS 8 in particular) can not read .vcf (= vcard) files. - * So I build a workaround to build a .ics (= vcalender) file. - * - * @return string - */ - public function getOutput() + public function getOutput(): string { - $output = ($this->isIOS7()) ? - $this->buildVCalendar() : $this->buildVCard(); - - return $output; + return $this->isIOS7() + ? $this->buildVCalendar() + : $this->buildVCard(); } - /** - * Get properties - * - * @return array - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * Has property - * - * @param string $key - * @return bool - */ - public function hasProperty($key) + public function hasProperty(string $key): bool { $properties = $this->getProperties(); @@ -845,104 +528,62 @@ public function hasProperty($key) return false; } - /** - * Is iOS - Check if the user is using an iOS-device - * - * @return bool - */ - public function isIOS() + public function isIOS(): bool { - // get user agent $browser = $this->getUserAgent(); - return (strpos($browser, 'iphone') || strpos($browser, 'ipod') || strpos($browser, 'ipad')); + return strpos($browser, 'iphone') || strpos($browser, 'ipod') || strpos($browser, 'ipad'); } - /** - * Is iOS less than 7 (should cal wrapper be returned) - * - * @return bool - */ - public function isIOS7() + public function isIOS7(): bool { - return ($this->isIOS() && $this->shouldAttachmentBeCal()); + return $this->isIOS() && $this->shouldAttachmentBeCal(); } - /** - * Save to a file - * - * @return void - */ - public function save() + public function save(): void { $file = $this->getFilename() . '.' . $this->getFileExtension(); - // Add save path if given - if (null !== $this->savePath) { + if ($this->savePath !== null) { $file = $this->savePath . $file; } file_put_contents( $file, - $this->getOutput() + $this->getOutput(), ); } - /** - * Set charset - * - * @param mixed $charset - * @return void - */ - public function setCharset($charset) + public function setCharset(string $charset): void { $this->charset = $charset; } - /** - * Set filename - * - * @param mixed $value - * @param bool $overwrite [optional] Default overwrite is true - * @param string $separator [optional] Default separator is an underscore '_' - * @return void - */ - public function setFilename($value, $overwrite = true, $separator = '_') - { - // recast to string if $value is array + public function setFilename( + string|array $value, + bool $overwrite = true, + string $separator = '_', + ): void { if (is_array($value)) { $value = implode($separator, $value); } - // trim unneeded values $value = trim($value, $separator); - // remove all spaces $value = preg_replace('/\s+/', $separator, $value); - // if value is empty, stop here if (empty($value)) { return; } - // decode value + lowercase the string $value = strtolower($this->decode($value)); - - // urlize this part $value = Transliterator::urlize($value); - - // overwrite filename or add to filename using a prefix in between - $this->filename = ($overwrite) ? - $value : $this->filename . $separator . $value; + $this->filename = $overwrite + ? $value + : $this->filename . $separator . $value; } - /** - * Set the save path directory - * - * @param string $savePath Save Path - * @throws VCardException - */ - public function setSavePath($savePath) + public function setSavePath(string $savePath): void { if (!is_dir($savePath)) { throw VCardException::outputDirectoryNotExists(); @@ -956,45 +597,27 @@ public function setSavePath($savePath) $this->savePath = $savePath; } - /** - * Set property - * - * @param string $element The element name you want to set, f.e.: name, email, phoneNumber, ... - * @param string $key - * @param string $value - * @throws VCardException - */ - private function setProperty($element, $key, $value) + private function setProperty(string $element, string $key, string $value): void { - if (!in_array($element, $this->multiplePropertiesForElementAllowed) - && isset($this->definedElements[$element]) - ) { - throw VCardException::elementAlreadyExists($element); + if (in_array($element, self::PROPERTY_MULTI_WHITELIST) === false && array_key_exists($element, $this->definedElements)) { + throw new InvalidArgumentException(sprintf('You can only add one %s property', $element)); } - // we define that we set this element $this->definedElements[$element] = true; - - // adding property $this->properties[] = [ 'key' => $key, - 'value' => $value + 'value' => $value, ]; } - /** - * Checks if we should return vcard in cal wrapper - * - * @return bool - */ - protected function shouldAttachmentBeCal() + protected function shouldAttachmentBeCal(): bool { $browser = $this->getUserAgent(); $matches = []; preg_match('/os (\d+)_(\d+)\s+/', $browser, $matches); - $version = isset($matches[1]) ? ((int)$matches[1]) : 999; + $version = isset($matches[1]) ? ((int) $matches[1]) : 999; - return ($version < 8); + return $version < 8; } } diff --git a/src/VCardException.php b/src/VCardException.php index 051ae7c..bb999f8 100644 --- a/src/VCardException.php +++ b/src/VCardException.php @@ -1,35 +1,27 @@ content = $content; $this->vcardObjects = []; $this->rewind(); $this->parse(); @@ -72,11 +39,9 @@ public function rewind(): void $this->position = 0; } - public function current(): \stdClass + public function current(): CardData { - if (! $this->valid()) { - throw new RuntimeException('invalid'); - } + Assert::notFalse($this->valid(), 'Current card should be valid, malformed input.'); return $this->getCardAtIndex($this->position); } @@ -88,7 +53,7 @@ public function key(): int public function next(): void { - $this->position++; + ++$this->position; } public function valid(): bool @@ -96,246 +61,219 @@ public function valid(): bool return !empty($this->vcardObjects[$this->position]); } - /** - * Fetch all the imported VCards. - * - * @return array - * A list of VCard card data objects. - */ public function getCards(): array { return $this->vcardObjects; } - /** - * Fetch the imported VCard at the specified index. - * - * @throws OutOfBoundsException - * - * @param int $i - * - * @return stdClass - * The card data object. - */ - public function getCardAtIndex($i): stdClass + public function getCardAtIndex(int $i): CardData { - if (isset($this->vcardObjects[$i])) { - return $this->vcardObjects[$i]; - } - throw new \OutOfBoundsException(); + Assert::keyExists($this->vcardObjects, $i); + + return $this->vcardObjects[$i]; } - /** - * Start the parsing process. - * - * This method will populate the data object. - */ - protected function parse() + protected function parse(): void { - // Normalize new lines. - $this->content = str_replace(["\r\n", "\r"], "\n", $this->content); - - // RFC2425 5.8.1. Line delimiting and folding - // Unfolding is accomplished by regarding CRLF immediately followed by - // a white space character (namely HTAB ASCII decimal 9 or. SPACE ASCII - // decimal 32) as equivalent to no characters at all (i.e., the CRLF - // and single white space character are removed). - $this->content = preg_replace("/\n(?:[ \t])/", "", $this->content); - $lines = explode("\n", $this->content); - - // Parse the VCard, line by line. + $content = $this->content; + Assert::string($content); + $content = str_replace(["\r\n", "\r"], "\n", $content); + Assert::string($content); + $content = preg_replace("/\n(?:[ \t])/", '', $content); + Assert::string($content); + $lines = array_filter(explode("\n", $content)); + + $cardData = null; foreach ($lines as $line) { $line = trim($line); - - if (strtoupper($line) == "BEGIN:VCARD") { - $cardData = new \stdClass(); - } elseif (strtoupper($line) == "END:VCARD") { + if (strtoupper($line) === VCardInterface::VCARD_START) { + $cardData = new CardData(); + } elseif (strtoupper($line) === VCardInterface::VCARD_END) { + Assert::notNull($cardData, 'Card data should not be null, malformed input.'); $this->vcardObjects[] = $cardData; } elseif (!empty($line)) { - // Strip grouping information. We don't use the group names. We - // simply use a list for entries that have multiple values. - // As per RFC, group names are alphanumerical, and end with a - // period (.). $line = preg_replace('/^\w+\./', '', $line); - - $type = ''; - $value = ''; - @list($type, $value) = explode(':', $line, 2); - + Assert::notNull($line, 'Line should not be null, malformed input.'); + Assert::contains($line, ':', 'Line should contain a colon, malformed input.'); + [$type, $value] = explode(':', $line, 2); $types = explode(';', $type); $element = strtoupper($types[0]); - array_shift($types); - // Normalize types. A type can either be a type-param directly, - // or can be prefixed with "type=". E.g.: "INTERNET" or - // "type=INTERNET". - if (!empty($types)) { - $types = array_map(function($type) { - return preg_replace('/^type=/i', '', $type); - }, $types); + if (empty($types) === false) { + $types = array_map(static fn ($type) => preg_replace('/^type=/i', '', $type), $types); } $i = 0; $rawValue = false; foreach ($types as $type) { - if (preg_match('/base64/', strtolower($type))) { - $value = base64_decode($value); - unset($types[$i]); - $rawValue = true; - } elseif (preg_match('/encoding=b/', strtolower($type))) { + Assert::string($type, 'Type should be a string, malformed input.'); + if (str_contains(strtolower($type), 'base64') || str_contains(strtolower($type), 'encoding=b')) { $value = base64_decode($value); unset($types[$i]); $rawValue = true; - } elseif (preg_match('/quoted-printable/', strtolower($type))) { + } elseif (str_contains(strtolower($type), 'quoted-printable')) { $value = quoted_printable_decode($value); unset($types[$i]); $rawValue = true; - } elseif (strpos(strtolower($type), 'charset=') === 0) { + } elseif (str_starts_with(strtolower($type), 'charset=')) { try { - $value = mb_convert_encoding($value, "UTF-8", substr($type, 8)); - } catch (\Exception $e) { + $value = mb_convert_encoding($value, 'UTF-8', substr($type, 8)); + } finally { + unset($types[$i]); } - unset($types[$i]); } - $i++; - } - switch (strtoupper($element)) { - case 'FN': - $cardData->fullname = $value; - break; - case 'N': - foreach ($this->parseName($value) as $key => $val) { - $cardData->{$key} = $val; - } - break; - case 'BDAY': - $cardData->birthday = $this->parseBirthday($value); - break; - case 'ADR': - if (!isset($cardData->address)) { - $cardData->address = []; - } - $key = !empty($types) ? implode(';', $types) : 'WORK;POSTAL'; - $cardData->address[$key][] = $this->parseAddress($value); - break; - case 'TEL': - if (!isset($cardData->phone)) { - $cardData->phone = []; - } - $key = !empty($types) ? implode(';', $types) : 'default'; - $cardData->phone[$key][] = $value; - break; - case 'EMAIL': - if (!isset($cardData->email)) { - $cardData->email = []; - } - $key = !empty($types) ? implode(';', $types) : 'default'; - $cardData->email[$key][] = $value; - break; - case 'REV': - $cardData->revision = $value; - break; - case 'VERSION': - $cardData->version = $value; - break; - case 'ORG': - $cardData->organization = $value; - break; - case 'URL': - if (!isset($cardData->url)) { - $cardData->url = []; - } - $key = !empty($types) ? implode(';', $types) : 'default'; - $cardData->url[$key][] = $value; - break; - case 'TITLE': - $cardData->title = $value; - break; - case 'PHOTO': - if ($rawValue) { - $cardData->rawPhoto = $value; - } else { - $cardData->photo = $value; - } - break; - case 'LOGO': - if ($rawValue) { - $cardData->rawLogo = $value; - } else { - $cardData->logo = $value; - } - break; - case 'NOTE': - $cardData->note = $this->unescape($value); - break; - case 'CATEGORIES': - $cardData->categories = array_map('trim', explode(',', $value)); - break; - case 'LABEL': - $cardData->label = $value; - break; + ++$i; } + + Assert::isInstanceOf($cardData, CardData::class, 'Card data should be an instance of CardData, malformed input.'); + $this->processElement( + $element, + $value, + $cardData, + $types, + $rawValue, + ); } } } - protected function parseName($value) + private function parseName(string $value): array { - @list( - $lastname, - $firstname, - $additional, - $prefix, - $suffix - ) = explode(';', $value); - return (object) [ - 'lastname' => $lastname, - 'firstname' => $firstname, - 'additional' => $additional, - 'prefix' => $prefix, - 'suffix' => $suffix, - ]; + $value = explode(';', $value); + $keys = ['lastname', 'firstname', 'additional', 'prefix', 'suffix']; + $value = array_pad($value, count($keys), ''); + + return array_combine($keys, $value); } - protected function parseBirthday($value) + private function parseBirthday(string $value): DateTimeImmutable { - return new \DateTime($value); + return new \DateTimeImmutable($value); } - protected function parseAddress($value) + private function parseAddress(string $value): array { - @list( - $name, - $extended, - $street, - $city, - $region, - $zip, - $country, - ) = explode(';', $value); - return (object) [ - 'name' => $name, - 'extended' => $extended, - 'street' => $street, - 'city' => $city, - 'region' => $region, - 'zip' => $zip, - 'country' => $country, - ]; + $value = explode(';', $value); + $keys = ['name', 'extended', 'street', 'city', 'region', 'zip', 'country']; + $value = array_pad($value, count($keys), ''); + + return array_combine($keys, $value); } /** - * Unescape newline characters according to RFC2425 section 5.8.4. - * This function will replace escaped line breaks with PHP_EOL. - * - * @link http://tools.ietf.org/html/rfc2425#section-5.8.4 - * @param string $text - * @return string + * @see http://tools.ietf.org/html/rfc2425#section-5.8.4 */ - protected function unescape($text) + protected function unescape(string $text): string { - return str_replace("\\n", PHP_EOL, $text); + return str_replace('\\n', \PHP_EOL, $text); + } + + private function processElement( + string $element, + array|false|string|null $value, + CardData $cardData, + array $types, + bool $rawValue, + ): void { + Assert::string($value, 'Value should be a string, malformed input.'); + + switch (strtoupper($element)) { + case 'FN': + $cardData->setName($value); + + break; + case 'N': + $nameData = $this->parseName($value); + $cardData->setLastName($nameData['lastname']); + $cardData->setFirstName($nameData['firstname']); + $cardData->setAdditional($nameData['additional']); + $cardData->setPrefix($nameData['prefix']); + $cardData->setSuffix($nameData['suffix']); + + break; + case 'BDAY': + $cardData->setBirthday($this->parseBirthday($value)); + + break; + case 'ADR': + $key = array_filter($types) !== [] + ? implode(';', $types) + : 'WORK;POSTAL'; + + $address = new AddressData(...array_values($this->parseAddress($value))); + $cardData->addAddress($key, $address); + + break; + case 'TEL': + $key = array_filter($types) !== [] + ? implode(';', $types) + : 'default'; + + $cardData->addPhone($key, $value); + + break; + case 'EMAIL': + $key = array_filter($types) !== [] + ? implode(';', $types) + : 'default'; + + $cardData->addEmail($key, $value); + + break; + case 'REV': + $cardData->setRevision($value); + + break; + case 'VERSION': + $cardData->setVersion($value); + + break; + case 'ORG': + $cardData->setOrganization($value); + + break; + case 'URL': + $key = array_filter($types) !== [] + ? implode(';', $types) + : 'default'; + + $cardData->addUrl($key, $value); + + break; + case 'TITLE': + $cardData->setTitle($value); + + break; + case 'PHOTO': + if ($rawValue) { + $cardData->setRawPhoto($value); + } else { + $cardData->setPhoto($value); + } + + break; + case 'LOGO': + if ($rawValue) { + $cardData->setRawLogo($value); + } else { + $cardData->setLogo($value); + } + + break; + case 'NOTE': + $cardData->setNote($this->unescape($value)); + + break; + case 'CATEGORIES': + $cardData->setCategories(array_map(static fn ($v) => trim($v), explode(',', $value))); + + break; + case 'LABEL': + $cardData->setLabel($value); + + break; + } } } diff --git a/tests/VCardExceptionTest.php b/tests/VCardExceptionTest.php deleted file mode 100644 index 33d16aa..0000000 --- a/tests/VCardExceptionTest.php +++ /dev/null @@ -1,24 +0,0 @@ -expectException(\JeroenDesloovere\VCard\VCardException::class); - throw new VCardException('Testing the VCard error.'); - } -} diff --git a/tests/VCardParserTest.php b/tests/VCardParserTest.php deleted file mode 100644 index dbac871..0000000 --- a/tests/VCardParserTest.php +++ /dev/null @@ -1,296 +0,0 @@ -expectException(OutOfBoundsException::class); - $parser = new VCardParser(''); - $parser->getCardAtIndex(2); - } - - public function testSimpleVcard() - { - $vcard = new VCard(); - $vcard->addName("Desloovere", "Jeroen"); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->firstname, "Jeroen"); - $this->assertEquals($parser->getCardAtIndex(0)->lastname, "Desloovere"); - $this->assertEquals($parser->getCardAtIndex(0)->fullname, "Jeroen Desloovere"); - } - - public function testBDay() - { - $vcard = new VCard(); - $vcard->addBirthday('31-12-2015'); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->birthday->format('Y-m-d'), '2015-12-31'); - } - - public function testAddress() - { - $vcard = new VCard(); - $vcard->addAddress( - "Lorem Corp.", - "(extended info)", - "54th Ipsum Street", - "PHPsville", - "Guacamole", - "01158", - "Gitland", - 'WORK;POSTAL' - ); - $vcard->addAddress( - "Jeroen Desloovere", - "(extended info, again)", - "25th Some Address", - "Townsville", - "Area 51", - "045784", - "Europe (is a country, right?)", - 'WORK;PERSONAL' - ); - $vcard->addAddress( - "Georges Desloovere", - "(extended info, again, again)", - "26th Some Address", - "Townsville-South", - "Area 51B", - "04554", - "Europe (no, it isn't)", - 'WORK;PERSONAL' - ); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->address['WORK;POSTAL'][0], (object) array( - 'name' => "Lorem Corp.", - 'extended' => "(extended info)", - 'street' => "54th Ipsum Street", - 'city' => "PHPsville", - 'region' => "Guacamole", - 'zip' => "01158", - 'country' => "Gitland", - )); - $this->assertEquals($parser->getCardAtIndex(0)->address['WORK;PERSONAL'][0], (object) array( - 'name' => "Jeroen Desloovere", - 'extended' => "(extended info, again)", - 'street' => "25th Some Address", - 'city' => "Townsville", - 'region' => "Area 51", - 'zip' => "045784", - 'country' => "Europe (is a country, right?)", - )); - $this->assertEquals($parser->getCardAtIndex(0)->address['WORK;PERSONAL'][1], (object) array( - 'name' => "Georges Desloovere", - 'extended' => "(extended info, again, again)", - 'street' => "26th Some Address", - 'city' => "Townsville-South", - 'region' => "Area 51B", - 'zip' => "04554", - 'country' => "Europe (no, it isn't)", - )); - } - - public function testPhone() - { - $vcard = new VCard(); - $vcard->addPhoneNumber('0984456123'); - $vcard->addPhoneNumber('2015123487', 'WORK'); - $vcard->addPhoneNumber('4875446578', 'WORK'); - $vcard->addPhoneNumber('9875445464', 'PREF;WORK;VOICE'); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->phone['default'][0], '0984456123'); - $this->assertEquals($parser->getCardAtIndex(0)->phone['WORK'][0], '2015123487'); - $this->assertEquals($parser->getCardAtIndex(0)->phone['WORK'][1], '4875446578'); - $this->assertEquals($parser->getCardAtIndex(0)->phone['PREF;WORK;VOICE'][0], '9875445464'); - } - - public function testEmail() - { - $vcard = new VCard(); - $vcard->addEmail('some@email.com'); - $vcard->addEmail('site@corp.net', 'WORK'); - $vcard->addEmail('site.corp@corp.net', 'WORK'); - $vcard->addEmail('support@info.info', 'PREF;WORK'); - $parser = new VCardParser($vcard->buildVCard()); - // The VCard class uses a default type of "INTERNET", so we do not test - // against the "default" key. - $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET'][0], 'some@email.com'); - $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET;WORK'][0], 'site@corp.net'); - $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET;WORK'][1], 'site.corp@corp.net'); - $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET;PREF;WORK'][0], 'support@info.info'); - } - - public function testOrganization() - { - $vcard = new VCard(); - $vcard->addCompany('Lorem Corp.'); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->organization, 'Lorem Corp.'); - } - - public function testUrl() - { - $vcard = new VCard(); - $vcard->addUrl('http://www.jeroendesloovere.be'); - $vcard->addUrl('http://home.example.com', 'HOME'); - $vcard->addUrl('http://work1.example.com', 'PREF;WORK'); - $vcard->addUrl('http://work2.example.com', 'PREF;WORK'); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->url['default'][0], 'http://www.jeroendesloovere.be'); - $this->assertEquals($parser->getCardAtIndex(0)->url['HOME'][0], 'http://home.example.com'); - $this->assertEquals($parser->getCardAtIndex(0)->url['PREF;WORK'][0], 'http://work1.example.com'); - $this->assertEquals($parser->getCardAtIndex(0)->url['PREF;WORK'][1], 'http://work2.example.com'); - } - - public function testNote() - { - $vcard = new VCard(); - $vcard->addNote('This is a testnote'); - $parser = new VCardParser($vcard->buildVCard()); - - $vcardMultiline = new VCard(); - $vcardMultiline->addNote("This is a multiline note\nNew line content!\r\nLine 2"); - $parserMultiline = new VCardParser($vcardMultiline->buildVCard()); - - $this->assertEquals($parser->getCardAtIndex(0)->note, 'This is a testnote'); - $this->assertEquals(nl2br($parserMultiline->getCardAtIndex(0)->note), nl2br("This is a multiline note" . PHP_EOL . "New line content!" . PHP_EOL . "Line 2")); - } - - public function testCategories() - { - $vcard = new VCard(); - $vcard->addCategories([ - 'Category 1', - 'cat-2', - 'another long category!' - ]); - $parser = new VCardParser($vcard->buildVCard()); - - $this->assertEquals($parser->getCardAtIndex(0)->categories[0], 'Category 1'); - $this->assertEquals($parser->getCardAtIndex(0)->categories[1], 'cat-2'); - $this->assertEquals($parser->getCardAtIndex(0)->categories[2], 'another long category!'); - } - - public function testTitle() - { - $vcard = new VCard(); - $vcard->addJobtitle('Ninja'); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->title, 'Ninja'); - } - - public function testLogo() - { - $image = __DIR__ . '/image.jpg'; - $imageUrl = 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg'; - - $vcard = new VCard(); - $vcard->addLogo($image, true); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->rawLogo, file_get_contents($image)); - - $vcard = new VCard(); - $vcard->addLogo($image, false); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->logo, __DIR__ . '/image.jpg'); - - $vcard = new VCard(); - $vcard->addLogo($imageUrl, false); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->logo, $imageUrl); - } - - public function testPhoto() - { - $image = __DIR__ . '/image.jpg'; - $imageUrl = 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg'; - - $vcard = new VCard(); - $vcard->addPhoto($image, true); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->rawPhoto, file_get_contents($image)); - - $vcard = new VCard(); - $vcard->addPhoto($image, false); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->photo, __DIR__ . '/image.jpg'); - - $vcard = new VCard(); - $vcard->addPhoto($imageUrl, false); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->photo, $imageUrl); - } - - public function testVcardDB() - { - $db = ''; - $vcard = new VCard(); - $vcard->addName("Desloovere", "Jeroen"); - $db .= $vcard->buildVCard(); - - $vcard = new VCard(); - $vcard->addName("Lorem", "Ipsum"); - $db .= $vcard->buildVCard(); - - $parser = new VCardParser($db); - $this->assertEquals($parser->getCardAtIndex(0)->fullname, "Jeroen Desloovere"); - $this->assertEquals($parser->getCardAtIndex(1)->fullname, "Ipsum Lorem"); - } - - public function testIteration() - { - // Prepare a VCard DB. - $db = ''; - $vcard = new VCard(); - $vcard->addName("Desloovere", "Jeroen"); - $db .= $vcard->buildVCard(); - - $vcard = new VCard(); - $vcard->addName("Lorem", "Ipsum"); - $db .= $vcard->buildVCard(); - - $parser = new VCardParser($db); - foreach ($parser as $i => $card) { - $this->assertEquals($card->fullname, $i == 0 ? "Jeroen Desloovere" : "Ipsum Lorem"); - } - } - - public function testFromFile() - { - $parser = VCardParser::parseFromFile(__DIR__ . '/example.vcf'); - // Use this opportunity to test fetching all cards directly. - $cards = $parser->getCards(); - $this->assertEquals($cards[0]->firstname, "Jeroen"); - $this->assertEquals($cards[0]->lastname, "Desloovere"); - $this->assertEquals($cards[0]->fullname, "Jeroen Desloovere"); - // Check the parsing of grouped items as well, which are present in the - // example file. - $this->assertEquals($cards[0]->url['default'][0], 'http://www.jeroendesloovere.be'); - $this->assertEquals($cards[0]->email['INTERNET'][0], 'site@example.com'); - } - - public function testFileNotFound() - { - $this->expectException(\RuntimeException::class); - $parser = VCardParser::parseFromFile(__DIR__ . '/does-not-exist.vcf'); - } - - public function testLabel() - { - $label = 'street, worktown, workpostcode Belgium'; - $vcard = new VCard(); - $vcard->addLabel($label, 'work'); - $parser = new VCardParser($vcard->buildVCard()); - $this->assertEquals($parser->getCardAtIndex(0)->label, $label); - } -} diff --git a/tests/VCardTest.php b/tests/VCardTest.php deleted file mode 100644 index ce481b1..0000000 --- a/tests/VCardTest.php +++ /dev/null @@ -1,466 +0,0 @@ - 'john@work.com']], - [['WORK' => 'john@work.com', 'HOME' => 'john@home.com']], - [['PREF;WORK' => 'john@work.com', 'HOME' => 'john@home.com']], - ]; - } - - /** - * Set up before class - * - * @return void - */ - protected function setUp(): void - { - // set timezone - date_default_timezone_set('Europe/Brussels'); - - $this->vcard = new VCard(); - - $this->firstName = 'Jeroen'; - $this->lastName = 'Desloovere'; - $this->additional = '&'; - $this->prefix = 'Mister'; - $this->suffix = 'Junior'; - - $this->emailAddress1 = ''; - $this->emailAddress2 = ''; - - $this->firstName2 = 'Ali'; - $this->lastName2 = 'ÖZSÜT'; - - $this->firstName3 = 'Garçon'; - $this->lastName3 = 'Jéroèn'; - } - - /** - * Tear down after class - */ - protected function tearDown(): void - { - $this->vcard = null; - } - - public function testAddAddress() - { - $this->assertEquals($this->vcard, $this->vcard->addAddress( - '', - '88th Floor', - '555 East Flours Street', - 'Los Angeles', - 'CA', - '55555', - 'USA' - )); - $this->assertStringContainsString('ADR;WORK;POSTAL;CHARSET=utf-8:;88th Floor;555 East Flours Street;Los Angele', $this->vcard->getOutput()); - // Should fold on row 75, so we should not see the full address. - $this->assertStringNotContainsString('ADR;WORK;POSTAL;CHARSET=utf-8:;88th Floor;555 East Flours Street;Los Angeles;CA;55555;', $this->vcard->getOutput()); - } - - public function testAddBirthday() - { - $this->assertEquals($this->vcard, $this->vcard->addBirthday('')); - } - - public function testAddCompany() - { - $this->assertEquals($this->vcard, $this->vcard->addCompany('')); - } - - public function testAddCategories() - { - $this->assertEquals($this->vcard, $this->vcard->addCategories([])); - } - - public function testAddEmail() - { - $this->assertEquals($this->vcard, $this->vcard->addEmail($this->emailAddress1)); - $this->assertEquals($this->vcard, $this->vcard->addEmail($this->emailAddress2)); - $this->assertEquals(2, count($this->vcard->getProperties())); - } - - public function testAddJobTitle() - { - $this->assertEquals($this->vcard, $this->vcard->addJobtitle('')); - } - - public function testAddRole() - { - $this->assertEquals($this->vcard, $this->vcard->addRole('')); - } - - public function testAddName() - { - $this->assertEquals($this->vcard, $this->vcard->addName('')); - } - - public function testAddNote() - { - $this->assertEquals($this->vcard, $this->vcard->addNote('')); - } - - public function testAddPhoneNumber() - { - $this->assertEquals($this->vcard, $this->vcard->addPhoneNumber('')); - $this->assertEquals($this->vcard, $this->vcard->addPhoneNumber('')); - $this->assertCount(2, $this->vcard->getProperties()); - } - - public function testAddPhotoWithJpgPhoto() - { - $return = $this->vcard->addPhoto(__DIR__ . '/image.jpg', true); - - $this->assertEquals($this->vcard, $return); - } - - public function testAddPhotoWithRemoteJpgPhoto() - { - $return = $this->vcard->addPhoto( - 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg', - true - ); - - $this->assertEquals($this->vcard, $return); - } - - /** - * Test adding remote empty photo - */ - public function testAddPhotoWithRemoteEmptyJpgPhoto() - { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Returned data is not an image.'); - $this->vcard->addPhoto( - 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/empty.jpg', - true - ); - } - - public function testAddPhotoContentWithJpgPhoto() - { - $return = $this->vcard->addPhotoContent(file_get_contents(__DIR__ . '/image.jpg')); - - $this->assertEquals($this->vcard, $return); - } - - /** - * Test adding empty photo - */ - public function testAddPhotoContentWithEmptyContent() - { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Returned data is not an image.'); - $this->vcard->addPhotoContent(''); - } - - public function testAddLogoWithJpgImage() - { - $return = $this->vcard->addLogo(__DIR__ . '/image.jpg', true); - - $this->assertEquals($this->vcard, $return); - } - - public function testAddLogoWithJpgImageNoInclude() - { - $return = $this->vcard->addLogo(__DIR__ . '/image.jpg', false); - - $this->assertEquals($this->vcard, $return); - } - - public function testAddLogoContentWithJpgImage() - { - $return = $this->vcard->addLogoContent(file_get_contents(__DIR__ . '/image.jpg')); - - $this->assertEquals($this->vcard, $return); - } - - /** - * Test adding empty photo - */ - public function testAddLogoContentWithEmptyContent() - { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Returned data is not an image.'); - $this->vcard->addLogoContent(''); - } - - public function testAddUrl() - { - $this->assertEquals($this->vcard, $this->vcard->addUrl('1')); - $this->assertEquals($this->vcard, $this->vcard->addUrl('2')); - $this->assertCount(2, $this->vcard->getProperties()); - } - - /** - * Test adding local photo using an empty file - */ - public function testAddPhotoWithEmptyFile() - { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Returned data is not an image.'); - $this->vcard->addPhoto(__DIR__ . '/emptyfile', true); - } - - /** - * Test adding logo with no value - */ - public function testAddLogoWithNoValue() - { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Returned data is not an image.'); - $this->vcard->addLogo(__DIR__ . '/emptyfile', true); - } - - /** - * Test adding photo with no photo - */ - public function testAddPhotoWithNoPhoto() - { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Returned data is not an image.'); - $this->vcard->addPhoto(__DIR__ . '/wrongfile', true); - } - - /** - * Test adding logo with no image - */ - public function testAddLogoWithNoImage() - { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Returned data is not an image.'); - $this->vcard->addLogo(__DIR__ . '/wrongfile', true); - } - - /** - * Test charset - */ - public function testCharset() - { - $charset = 'ISO-8859-1'; - $this->vcard->setCharset($charset); - $this->assertEquals($charset, $this->vcard->getCharset()); - } - - /** - * Test Email - * - * @dataProvider emailDataProvider $emails - */ - public function testEmail($emails = []) - { - foreach ($emails as $key => $email) { - if (is_string($key)) { - $this->vcard->addEmail($email, $key); - } else { - $this->vcard->addEmail($email); - } - } - - foreach ($emails as $key => $email) { - if (is_string($key)) { - $this->assertStringContainsString('EMAIL;INTERNET;' . $key . ':' . $email, $this->vcard->getOutput()); - } else { - $this->assertStringContainsString('EMAIL;INTERNET:' . $email, $this->vcard->getOutput()); - } - } - } - - /** - * Test first name and last name - */ - public function testFirstNameAndLastName() - { - $this->vcard->addName( - $this->lastName, - $this->firstName - ); - - $this->assertEquals('jeroen-desloovere', $this->vcard->getFilename()); - } - - /** - * Test full blown name - */ - public function testFullBlownName() - { - $this->vcard->addName( - $this->lastName, - $this->firstName, - $this->additional, - $this->prefix, - $this->suffix - ); - - $this->assertEquals('mister-jeroen-desloovere-junior', $this->vcard->getFilename()); - } - - /** - * Test multiple birthdays - */ - public function testMultipleBirthdays() - { - $this->expectException(\Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addBirthday('1')); - $this->expectException(Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addBirthday('2')); - } - - /** - * Test multiple categories - */ - public function testMultipleCategories() - { - $this->expectException(\Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addCategories(['1'])); - $this->expectException(Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addCategories(['2'])); - } - - /** - * Test multiple companies - */ - public function testMultipleCompanies() - { - $this->expectException(\Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addCompany('1')); - $this->expectException(Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addCompany('2')); - } - - /** - * Test multiple job titles - */ - public function testMultipleJobtitles() - { - $this->expectException(\Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addJobtitle('1')); - $this->expectException(Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addJobtitle('2')); - } - - /** - * Test multiple roles - */ - public function testMultipleRoles() - { - $this->expectException(\Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addRole('1')); - $this->expectException(Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addRole('2')); - } - - /** - * Test multiple names - */ - public function testMultipleNames() - { - $this->expectException(\Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addName('1')); - $this->expectException(Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addName('2')); - } - - /** - * Test multiple notes - */ - public function testMultipleNotes() - { - $this->expectException(\Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addNote('1')); - $this->expectException(Exception::class); - $this->assertEquals($this->vcard, $this->vcard->addNote('2')); - } - - /** - * Test special first name and last name - */ - public function testSpecialFirstNameAndLastName() - { - $this->vcard->addName( - $this->lastName2, - $this->firstName2 - ); - - $this->assertEquals('ali-ozsut', $this->vcard->getFilename()); - } - - /** - * Test special first name and last name - */ - public function testSpecialFirstNameAndLastName2() - { - $this->vcard->addName( - $this->lastName3, - $this->firstName3 - ); - - $this->assertEquals('garcon-jeroen', $this->vcard->getFilename()); - } - - /** - * Test multiple labels - */ - public function testMultipleLabels() - { - $this->assertSame($this->vcard, $this->vcard->addLabel('My label')); - $this->assertSame($this->vcard, $this->vcard->addLabel('My work label', 'WORK')); - $this->assertSame(2, count($this->vcard->getProperties())); - $this->assertStringContainsString('LABEL;CHARSET=utf-8:My label', $this->vcard->getOutput()); - $this->assertStringContainsString('LABEL;WORK;CHARSET=utf-8:My work label', $this->vcard->getOutput()); - } - - public function testChunkSplitUnicode() - { - $class_handler = new \ReflectionClass('JeroenDesloovere\VCard\VCard'); - $method_handler = $class_handler->getMethod('chunk_split_unicode'); - $method_handler->setAccessible(true); - - $ascii_input="Lorem ipsum dolor sit amet,"; - $ascii_output = $method_handler->invokeArgs(new VCard(), [$ascii_input,10,'|']); - $unicode_input='Τη γλώσσα μου έδωσαν ελληνική το σπίτι φτωχικό στις αμμουδιές του Ομήρου.'; - $unicode_output = $method_handler->invokeArgs(new VCard(), [$unicode_input,10,'|']); - - $this->assertEquals( - "Lorem ipsu|m dolor si|t amet,|", - $ascii_output); - $this->assertEquals( - "Τη γλώσσα |μου έδωσαν| ελληνική |το σπίτι φ|τωχικό στι|ς αμμουδιέ|ς του Ομήρ|ου.|", - $unicode_output); - } -} From c53989ccde592b77c511c2c1ed5dbd1cac1be4a8 Mon Sep 17 00:00:00 2001 From: Maximilian Graf Schimmelmann Date: Mon, 8 Jan 2024 17:39:44 +0100 Subject: [PATCH 2/6] Down composer php version to test all legit php classes. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 13dc5a6..a8720a3 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ } ], "require": { - "php": "^8.2", + "php": "^8.1", "behat/transliterator": "~1.0", "webmozart/assert": "^1.11", "ext-fileinfo": "*", From df493ac73c8ba1ee8ba787f452279aae88ac34a4 Mon Sep 17 00:00:00 2001 From: Maximilian Graf Schimmelmann Date: Mon, 8 Jan 2024 17:40:34 +0100 Subject: [PATCH 3/6] Fix Actions --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3927e05..9c214c9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,16 +40,16 @@ jobs: run: "composer install --no-interaction --no-progress --no-suggest" - name: "EasyCodingStandards for src" - run: "vendor/bin/ecs check src/ tests/ --no-interaction --no-progress-bar" + run: "vendor/bin/ecs check src/ Tests/ --no-interaction --no-progress-bar" - name: "PhpStan for src/" run: "vendor/bin/phpstan analyse --error-format=checkstyle src --level=8 | cs2pr" - - name: "PhpStan for tests/" - run: "vendor/bin/phpstan analyse --error-format=checkstyle tests --level=6 | cs2pr" + - name: "PhpStan for Tests/" + run: "vendor/bin/phpstan analyse --error-format=checkstyle Tests/ --level=6 | cs2pr" - name: "PHPUnit Test with Coverage" - run: "vendor/bin/phpunit -c phpunit.xml.dist tests/ --coverage-clover=clover.xml" + run: "vendor/bin/phpunit -c phpunit.xml.dist Tests/ --coverage-clover=clover.xml" # - name: Upload coverage reports to Codecov # uses: codecov/codecov-action@v3 From 704c862c6677cbfda178ac57312e74e9e1d7c8eb Mon Sep 17 00:00:00 2001 From: Maximilian Graf Schimmelmann Date: Mon, 8 Jan 2024 17:44:05 +0100 Subject: [PATCH 4/6] Make AddressData Class compatible with PHP8.1 --- src/Dto/AddressData.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Dto/AddressData.php b/src/Dto/AddressData.php index a1e948d..91be4bc 100644 --- a/src/Dto/AddressData.php +++ b/src/Dto/AddressData.php @@ -4,16 +4,16 @@ namespace JeroenDesloovere\VCard\Dto; -final readonly class AddressData +final class AddressData { public function __construct( - private string $name, - private string $extended, - private string $street, - private string $city, - private string $region, - private string $zip, - private string $country, + private readonly string $name, + private readonly string $extended, + private readonly string $street, + private readonly string $city, + private readonly string $region, + private readonly string $zip, + private readonly string $country, ) { } From 7099bc5860264b7f764a9fb40fc1dd96f2c6cb51 Mon Sep 17 00:00:00 2001 From: Maximilian Graf Schimmelmann Date: Mon, 8 Jan 2024 17:53:25 +0100 Subject: [PATCH 5/6] Adapt PHPUnit to succesfully return coverage result --- .github/workflows/ci.yaml | 8 +++--- build/php/etc/php.ini | 2 +- composer.json | 2 +- phpunit.xml.dist | 36 +++++++++++------------- {Tests => tests}/VCardExceptionTest.php | 3 +- {Tests => tests}/VCardParserTest.php | 23 ++++++++------- {Tests => tests}/VCardTest.php | 0 {Tests => tests}/empty.jpg | 0 {Tests => tests}/emptyfile | 0 {Tests => tests}/example.vcf | 0 {Tests => tests}/image.jpg | Bin {Tests => tests}/wrongfile | 0 12 files changed, 37 insertions(+), 37 deletions(-) rename {Tests => tests}/VCardExceptionTest.php (77%) rename {Tests => tests}/VCardParserTest.php (94%) rename {Tests => tests}/VCardTest.php (100%) rename {Tests => tests}/empty.jpg (100%) rename {Tests => tests}/emptyfile (100%) rename {Tests => tests}/example.vcf (100%) rename {Tests => tests}/image.jpg (100%) rename {Tests => tests}/wrongfile (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9c214c9..2cce762 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,16 +40,16 @@ jobs: run: "composer install --no-interaction --no-progress --no-suggest" - name: "EasyCodingStandards for src" - run: "vendor/bin/ecs check src/ Tests/ --no-interaction --no-progress-bar" + run: "vendor/bin/ecs check src/ tests/ --no-interaction --no-progress-bar" - name: "PhpStan for src/" run: "vendor/bin/phpstan analyse --error-format=checkstyle src --level=8 | cs2pr" - - name: "PhpStan for Tests/" - run: "vendor/bin/phpstan analyse --error-format=checkstyle Tests/ --level=6 | cs2pr" + - name: "PhpStan for tests/" + run: "vendor/bin/phpstan analyse --error-format=checkstyle tests/ --level=6 | cs2pr" - name: "PHPUnit Test with Coverage" - run: "vendor/bin/phpunit -c phpunit.xml.dist Tests/ --coverage-clover=clover.xml" + run: "vendor/bin/phpunit -c phpunit.xml.dist tests/ --coverage-clover=clover.xml" # - name: Upload coverage reports to Codecov # uses: codecov/codecov-action@v3 diff --git a/build/php/etc/php.ini b/build/php/etc/php.ini index 5ca326f..23ef377 100644 --- a/build/php/etc/php.ini +++ b/build/php/etc/php.ini @@ -15,7 +15,7 @@ date.timezone = "Europe/Berlin" xdebug.idekey=PHPSTORM xdebug.max_nesting_level = 2048 -xdebug.mode=debug +xdebug.mode=develop,debug,coverage xdebug.client_port=9000 xdebug.client_host=host.docker.internal xdebug.start_with_request=yes diff --git a/composer.json b/composer.json index a8720a3..2c16fd5 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,6 @@ "psr-4": { "JeroenDesloovere\\VCard\\": "src/"} }, "autoload-dev": { - "psr-4": { "JeroenDesloovere\\VCard\\Tests\\": "Tests/"} + "psr-4": { "JeroenDesloovere\\VCard\\Tests\\": "tests/"} } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ebf6dd2..3490af1 100755 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,22 +1,18 @@ - - - - - tests - - - - - - src - - - - - - src - - - + + + + tests + + + + + src + + diff --git a/Tests/VCardExceptionTest.php b/tests/VCardExceptionTest.php similarity index 77% rename from Tests/VCardExceptionTest.php rename to tests/VCardExceptionTest.php index 15277af..9b2c7a7 100644 --- a/Tests/VCardExceptionTest.php +++ b/tests/VCardExceptionTest.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace JeroenDesloovere\VCard; +namespace JeroenDesloovere\VCard\Tests; +use JeroenDesloovere\VCard\VCardException; use PHPUnit\Framework\TestCase; final class VCardExceptionTest extends TestCase diff --git a/Tests/VCardParserTest.php b/tests/VCardParserTest.php similarity index 94% rename from Tests/VCardParserTest.php rename to tests/VCardParserTest.php index 56940fb..b72b050 100644 --- a/Tests/VCardParserTest.php +++ b/tests/VCardParserTest.php @@ -41,7 +41,10 @@ public function test_it_can_retrieve_the_birthday_in_the_right_format(): void $date = new DateTimeImmutable('01-01-2021'); $this->vcard->addBirthday($date); $parser = new VCardParser($this->vcard->buildVCard()); - $this->assertSame($parser->getCardAtIndex(0)->getBirthday()->format('Y-m-d H:i:s'), $date->format('Y-m-d H:i:s')); + $this->assertSame( + $parser->getCardAtIndex(0)->getBirthday()->format('Y-m-d H:i:s'), + $date->format('Y-m-d H:i:s') + ); } public function test_it_can_parse_addresses_correctly(): void @@ -175,7 +178,7 @@ public function test_it_can_set_and_get_the_cards_note_correctly(): void $this->assertSame($resolve, 'This is a testnote'); $resolve = $parserMultiline->getCardAtIndex(0)->getNote(); - $this->assertSame($resolve, 'This is a multiline note' . \PHP_EOL . 'New line content!' . \PHP_EOL . 'Line 2'); + $this->assertSame($resolve, 'This is a multiline note'.\PHP_EOL.'New line content!'.\PHP_EOL.'Line 2'); } public function test_it_can_set_and_get_categories_correctly(): void @@ -201,7 +204,7 @@ public function test_it_can_set_and_get_titlecorrectly(): void public function test_it_can_set_and_get_raw_logo_correctly(): void { - $image = __DIR__ . '/image.jpg'; + $image = __DIR__.'/image.jpg'; $imageUrl = 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg'; $card = new VCard(); @@ -212,7 +215,7 @@ public function test_it_can_set_and_get_raw_logo_correctly(): void $card = new VCard(); $card->addLogo($image, false); $parser = new VCardParser($card->buildVCard()); - $this->assertSame($parser->getCardAtIndex(0)->getLogo(), __DIR__ . '/image.jpg'); + $this->assertSame($parser->getCardAtIndex(0)->getLogo(), __DIR__.'/image.jpg'); $card = new VCard(); $card->addLogo($imageUrl, false); @@ -222,7 +225,7 @@ public function test_it_can_set_and_get_raw_logo_correctly(): void public function test_it_can_set_and_get_raw_photo_correctly(): void { - $image = __DIR__ . '/image.jpg'; + $image = __DIR__.'/image.jpg'; $imageUrl = 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg'; $card = new VCard(); @@ -233,7 +236,7 @@ public function test_it_can_set_and_get_raw_photo_correctly(): void $card = new VCard(); $card->addPhoto($image, false); $parser = new VCardParser($card->buildVCard()); - $this->assertSame($parser->getCardAtIndex(0)->getPhoto(), __DIR__ . '/image.jpg'); + $this->assertSame($parser->getCardAtIndex(0)->getPhoto(), __DIR__.'/image.jpg'); $card = new VCard(); $card->addPhoto($imageUrl, false); @@ -275,15 +278,15 @@ public function test_it_can_iterate_over_multiple_cards_correctly(): void $this->assertSame( $card->getName(), $i === 0 - ? 'Jeroen Desloovere' - : 'Ipsum Lorem', + ? 'Jeroen Desloovere' + : 'Ipsum Lorem', ); } } public function test_it_can_load_vcard_from_file(): void { - $parser = VCardParser::parseFromFile(__DIR__ . '/example.vcf'); + $parser = VCardParser::parseFromFile(__DIR__.'/example.vcf'); $cards = $parser->getCards(); foreach ($cards as $card) { $this->assertInstanceOf(CardData::class, $card); @@ -299,7 +302,7 @@ public function test_it_can_load_vcard_from_file(): void public function test_it_will_throw_an_exception_when_file_cannot_be_loaded(): void { $this->expectException(InvalidArgumentException::class); - VCardParser::parseFromFile(__DIR__ . '/does-not-exist.vcf'); + VCardParser::parseFromFile(__DIR__.'/does-not-exist.vcf'); } public function test_it_can_correctly_return_a_label(): void diff --git a/Tests/VCardTest.php b/tests/VCardTest.php similarity index 100% rename from Tests/VCardTest.php rename to tests/VCardTest.php diff --git a/Tests/empty.jpg b/tests/empty.jpg similarity index 100% rename from Tests/empty.jpg rename to tests/empty.jpg diff --git a/Tests/emptyfile b/tests/emptyfile similarity index 100% rename from Tests/emptyfile rename to tests/emptyfile diff --git a/Tests/example.vcf b/tests/example.vcf similarity index 100% rename from Tests/example.vcf rename to tests/example.vcf diff --git a/Tests/image.jpg b/tests/image.jpg similarity index 100% rename from Tests/image.jpg rename to tests/image.jpg diff --git a/Tests/wrongfile b/tests/wrongfile similarity index 100% rename from Tests/wrongfile rename to tests/wrongfile From 8ecf204e47e5487008f8be1ee7ae5c8c765df5e4 Mon Sep 17 00:00:00 2001 From: Maximilian Graf Schimmelmann Date: Mon, 8 Jan 2024 17:56:29 +0100 Subject: [PATCH 6/6] Apply ECS to tests --- tests/VCardParserTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/VCardParserTest.php b/tests/VCardParserTest.php index b72b050..477fc77 100644 --- a/tests/VCardParserTest.php +++ b/tests/VCardParserTest.php @@ -43,7 +43,7 @@ public function test_it_can_retrieve_the_birthday_in_the_right_format(): void $parser = new VCardParser($this->vcard->buildVCard()); $this->assertSame( $parser->getCardAtIndex(0)->getBirthday()->format('Y-m-d H:i:s'), - $date->format('Y-m-d H:i:s') + $date->format('Y-m-d H:i:s'), ); } @@ -178,7 +178,7 @@ public function test_it_can_set_and_get_the_cards_note_correctly(): void $this->assertSame($resolve, 'This is a testnote'); $resolve = $parserMultiline->getCardAtIndex(0)->getNote(); - $this->assertSame($resolve, 'This is a multiline note'.\PHP_EOL.'New line content!'.\PHP_EOL.'Line 2'); + $this->assertSame($resolve, 'This is a multiline note' . \PHP_EOL . 'New line content!' . \PHP_EOL . 'Line 2'); } public function test_it_can_set_and_get_categories_correctly(): void @@ -204,7 +204,7 @@ public function test_it_can_set_and_get_titlecorrectly(): void public function test_it_can_set_and_get_raw_logo_correctly(): void { - $image = __DIR__.'/image.jpg'; + $image = __DIR__ . '/image.jpg'; $imageUrl = 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg'; $card = new VCard(); @@ -215,7 +215,7 @@ public function test_it_can_set_and_get_raw_logo_correctly(): void $card = new VCard(); $card->addLogo($image, false); $parser = new VCardParser($card->buildVCard()); - $this->assertSame($parser->getCardAtIndex(0)->getLogo(), __DIR__.'/image.jpg'); + $this->assertSame($parser->getCardAtIndex(0)->getLogo(), __DIR__ . '/image.jpg'); $card = new VCard(); $card->addLogo($imageUrl, false); @@ -225,7 +225,7 @@ public function test_it_can_set_and_get_raw_logo_correctly(): void public function test_it_can_set_and_get_raw_photo_correctly(): void { - $image = __DIR__.'/image.jpg'; + $image = __DIR__ . '/image.jpg'; $imageUrl = 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg'; $card = new VCard(); @@ -236,7 +236,7 @@ public function test_it_can_set_and_get_raw_photo_correctly(): void $card = new VCard(); $card->addPhoto($image, false); $parser = new VCardParser($card->buildVCard()); - $this->assertSame($parser->getCardAtIndex(0)->getPhoto(), __DIR__.'/image.jpg'); + $this->assertSame($parser->getCardAtIndex(0)->getPhoto(), __DIR__ . '/image.jpg'); $card = new VCard(); $card->addPhoto($imageUrl, false); @@ -286,7 +286,7 @@ public function test_it_can_iterate_over_multiple_cards_correctly(): void public function test_it_can_load_vcard_from_file(): void { - $parser = VCardParser::parseFromFile(__DIR__.'/example.vcf'); + $parser = VCardParser::parseFromFile(__DIR__ . '/example.vcf'); $cards = $parser->getCards(); foreach ($cards as $card) { $this->assertInstanceOf(CardData::class, $card); @@ -302,7 +302,7 @@ public function test_it_can_load_vcard_from_file(): void public function test_it_will_throw_an_exception_when_file_cannot_be_loaded(): void { $this->expectException(InvalidArgumentException::class); - VCardParser::parseFromFile(__DIR__.'/does-not-exist.vcf'); + VCardParser::parseFromFile(__DIR__ . '/does-not-exist.vcf'); } public function test_it_can_correctly_return_a_label(): void