diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..dc4b6fa --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,49 @@ +on: + - push + +name: Run PHPStan checks + +jobs: + mutation: + name: PHPStan ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.2" + - "8.3" + - "8.4" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + coverage: pcov + ini-values: assert.exception=1, zend.assertions=1, error_reporting=-1, log_errors_max_len=0, display_errors=On + tools: composer:v2, cs2pr + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v4 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + + - name: Install dependencies with composer + run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run static analysis with PHPStan + run: vendor/bin/phpstan analyse diff --git a/README.md b/README.md index 696be56..33e7754 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,26 @@ # dot-mail -> [!IMPORTANT] +Dotkernel's mail service. + > dot-mail is a wrapper on top of [symfony mailer](https://github.com/symfony/mailer) -## dot-mail badges +## Documentation + +Documentation is available at: https://docs.dotkernel.org/dot-mail/. + +## Badges ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-mail) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-mail/5.2.1) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-mail/5.3.0) [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-mail)](https://github.com/dotkernel/dot-mail/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-mail)](https://github.com/dotkernel/dot-mail/network) [![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-mail)](https://github.com/dotkernel/dot-mail/stargazers) -[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-mail)](https://github.com/dotkernel/dot-mail/blob/5.0/LICENSE.md) +[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-mail)](https://github.com/dotkernel/dot-mail/blob/5.2/LICENSE.md) -[![Build Static](https://github.com/dotkernel/dot-mail/actions/workflows/continuous-integration.yml/badge.svg?branch=5.0)](https://github.com/dotkernel/dot-mail/actions/workflows/continuous-integration.yml) -[![codecov](https://codecov.io/gh/dotkernel/dot-mail/branch/5.0/graph/badge.svg?token=G51NEHYKD3)](https://codecov.io/gh/dotkernel/dot-mail) +[![Build Static](https://github.com/dotkernel/dot-mail/actions/workflows/continuous-integration.yml/badge.svg?branch=5.2)](https://github.com/dotkernel/dot-mail/actions/workflows/continuous-integration.yml) +[![codecov](https://codecov.io/gh/dotkernel/dot-mail/branch/5.2/graph/badge.svg?token=G51NEHYKD3)](https://codecov.io/gh/dotkernel/dot-mail) +[![PHPStan](https://github.com/dotkernel/dot-mail/actions/workflows/static-analysis.yml/badge.svg?branch=5.2)](https://github.com/dotkernel/dot-mail/actions/workflows/static-analysis.yml) ## Installation diff --git a/composer.json b/composer.json index 4d7416a..46398b8 100644 --- a/composer.json +++ b/composer.json @@ -37,8 +37,9 @@ "require-dev": { "laminas/laminas-coding-standard": "^3.0", "mikey179/vfsstream": "^v1.6.11", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^6.0" + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.2" }, "autoload": { "psr-4": { @@ -59,7 +60,6 @@ "cs-check": "phpcs", "cs-fix": "phpcbf", "test": "phpunit --colors=always", - "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", - "static-analysis": "psalm --shepherd --stats" + "static-analysis": "phpstan analyse --memory-limit 1G" } } diff --git a/docs/book/v5/overview.md b/docs/book/v5/overview.md index 706ac44..a1551cf 100644 --- a/docs/book/v5/overview.md +++ b/docs/book/v5/overview.md @@ -1,7 +1,23 @@ # Overview +Dotkernel's mail service. + > dot-mail is a wrapper on top of [symfony mailer](https://github.com/symfony/mailer) +## Badges + +![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-mail) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-mail/5.3.0) + +[![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-mail)](https://github.com/dotkernel/dot-mail/issues) +[![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-mail)](https://github.com/dotkernel/dot-mail/network) +[![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-mail)](https://github.com/dotkernel/dot-mail/stargazers) +[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-mail)](https://github.com/dotkernel/dot-mail/blob/5.2/LICENSE.md) + +[![Build Static](https://github.com/dotkernel/dot-mail/actions/workflows/continuous-integration.yml/badge.svg?branch=5.2)](https://github.com/dotkernel/dot-mail/actions/workflows/continuous-integration.yml) +[![codecov](https://codecov.io/gh/dotkernel/dot-mail/branch/5.2/graph/badge.svg?token=G51NEHYKD3)](https://codecov.io/gh/dotkernel/dot-mail) +[![PHPStan](https://github.com/dotkernel/dot-mail/actions/workflows/static-analysis.yml/badge.svg?branch=5.2)](https://github.com/dotkernel/dot-mail/actions/workflows/static-analysis.yml) + ## Extra features - the option to log the results of the mailing process it provides the developer with more information and greater control. diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..349be25 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon +parameters: + level: 5 + paths: + - src + - test + treatPhpDocTypesAsCertain: false diff --git a/psalm-baseline.xml b/psalm-baseline.xml deleted file mode 100644 index f2f9ee2..0000000 --- a/psalm-baseline.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index ed4c186..0000000 --- a/psalm.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/src/Email.php b/src/Email.php index f9bb236..547ff35 100644 --- a/src/Email.php +++ b/src/Email.php @@ -8,6 +8,7 @@ use DateTimeInterface; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Exception\LogicException; +use Symfony\Component\Mime\Header\MailboxListHeader; use Symfony\Component\Mime\Message; use Symfony\Component\Mime\Part\AbstractPart; use Symfony\Component\Mime\Part\Multipart\AlternativePart; @@ -46,13 +47,10 @@ class Email extends Message private ?string $textCharset = null; private ?string $html = null; - private ?string $htmlCharset = null; - private array $attachments = []; - private ?AbstractPart $cachedBody = null; + private ?string $htmlCharset = null; + private array $attachments = []; + private mixed $cachedBody; - /** - * @return $this - */ public function subject(string $subject): static { return $this->setHeaderBody('Text', 'Subject', $subject); @@ -63,9 +61,6 @@ public function getSubject(): ?string return $this->getHeaders()->getHeaderBody('Subject'); } - /** - * @return $this - */ public function date(DateTimeInterface $dateTime): static { return $this->setHeaderBody('Date', 'Date', $dateTime); @@ -76,9 +71,6 @@ public function getDate(): ?DateTimeImmutable return $this->getHeaders()->getHeaderBody('Date'); } - /** - * @return $this - */ public function returnPath(Address|string $address): static { return $this->setHeaderBody('Path', 'Return-Path', Address::create($address)); @@ -201,7 +193,7 @@ public function getPriority(): int { [$priority] = sscanf($this->getHeaders()->getHeaderBody('X-Priority') ?? '', '%[1-5]'); - return (int) $priority ?? 3; + return $priority !== null ? (int) $priority : 3; } public function text(string $body, string $charset = 'utf-8'): static @@ -315,7 +307,7 @@ private function generateBody(): AbstractPart return $part ?? new TextPart($this->text, $this->textCharset); } - private function prepareParts(): ?array + private function prepareParts(): array { $names = []; $htmlPart = null; @@ -373,7 +365,8 @@ private function setHeaderBody(string $type, string $name, mixed $body): static private function addListAddressHeaderBody(string $name, array $addresses): static { - if (! $header = $this->getHeaders()->get($name)) { + $header = $this->getHeaders()->get($name); + if (! $header instanceof MailboxListHeader) { return $this->setListAddressHeaderBody($name, $addresses); } $header->addAddresses(Address::createArray($addresses)); @@ -385,7 +378,7 @@ private function setListAddressHeaderBody(string $name, array $addresses): stati { $addresses = Address::createArray($addresses); $headers = $this->getHeaders(); - if ($header = $headers->get($name)) { + if (($header = $headers->get($name)) instanceof MailboxListHeader) { $header->setAddresses($addresses); } else { $headers->addMailboxListHeader($name, $addresses); diff --git a/test/Event/MailEventTest.php b/test/Event/MailEventTest.php index d10ee4a..d6323be 100644 --- a/test/Event/MailEventTest.php +++ b/test/Event/MailEventTest.php @@ -29,16 +29,16 @@ public function testMailEventCanBeCreated(): void { $defaultMailEvent = new MailEvent($this->mailService); - $this->assertInstanceOf(MailEvent::class, $defaultMailEvent); - $this->assertInstanceOf(MailServiceInterface::class, $defaultMailEvent->getMailService()); + $this->assertContainsOnlyInstancesOf(MailEvent::class, [$defaultMailEvent]); + $this->assertContainsOnlyInstancesOf(MailServiceInterface::class, [$defaultMailEvent->getMailService()]); $customMailEvent = new MailEvent($this->mailService, MailEvent::EVENT_MAIL_SEND_ERROR); $mailServiceInterface = $this->createMock(MailServiceInterface::class); $customMailEvent->setMailService($mailServiceInterface); - $this->assertInstanceOf(MailEvent::class, $customMailEvent); + $this->assertContainsOnlyInstancesOf(MailEvent::class, [$customMailEvent]); $this->assertSame(MailEvent::EVENT_MAIL_SEND_ERROR, $customMailEvent->getName()); - $this->assertInstanceOf(MailServiceInterface::class, $customMailEvent->getMailService()); + $this->assertContainsOnlyInstancesOf(MailServiceInterface::class, [$customMailEvent->getMailService()]); } public function testEventPropagation(): void diff --git a/test/Factory/MailOptionsAbstractFactoryTest.php b/test/Factory/MailOptionsAbstractFactoryTest.php index 650a48f..cdf2840 100644 --- a/test/Factory/MailOptionsAbstractFactoryTest.php +++ b/test/Factory/MailOptionsAbstractFactoryTest.php @@ -53,7 +53,7 @@ public function testGeneratesMailOptions(): void $subject = (new MailOptionsAbstractFactory())($container, $defaultName); - $this->assertInstanceOf(MailOptions::class, $subject); + $this->assertContainsOnlyInstancesOf(MailOptions::class, [$subject]); } /** @@ -73,6 +73,6 @@ public function testIsSpecificConfigArray(): void $subject = (new MailOptionsAbstractFactory())($container, $defaultName); - $this->assertInstanceOf(MailOptions::class, $subject); + $this->assertContainsOnlyInstancesOf(MailOptions::class, [$subject]); } } diff --git a/test/Factory/MailServiceAbstractFactoryTest.php b/test/Factory/MailServiceAbstractFactoryTest.php index a926bba..34177f2 100644 --- a/test/Factory/MailServiceAbstractFactoryTest.php +++ b/test/Factory/MailServiceAbstractFactoryTest.php @@ -127,7 +127,7 @@ public function testGenerateServiceWithProvidedAdapter(): void ->method('getSmtpOptions') ->willReturn($this->smtpOptions); - $mailService = (new Subject())($this->container, $requestedName); + $mailService = ($this->subject)($this->container, $requestedName); $this->assertInstanceOf(MailService::class, $mailService); $this->assertInstanceOf(EsmtpTransport::class, $mailService->getTransport()); } @@ -197,7 +197,7 @@ public function testGenerateServiceWithoutProvidedAdapter(): void $this->expectException(RuntimeException::class); - $mailService = (new Subject())($this->container, $requestedName); + $mailService = ($this->subject)($this->container, $requestedName); $this->assertInstanceOf(MailService::class, $mailService); $this->assertInstanceOf(EsmtpTransport::class, $mailService->getTransport()); @@ -226,7 +226,7 @@ public function testGenerateServiceInvalidAdapter(): void ]); $this->expectException(InvalidArgumentException::class); - (new Subject())($this->container, $requestedName); + ($this->subject)($this->container, $requestedName); } /** @@ -258,6 +258,6 @@ public function testGenerateServiceThrowException(): void ]); $this->expectException(InvalidArgumentException::class); - (new Subject())($this->container, $requestedName); + ($this->subject)($this->container, $requestedName); } } diff --git a/test/Options/MailOptionsTest.php b/test/Options/MailOptionsTest.php index 5e78703..4382364 100644 --- a/test/Options/MailOptionsTest.php +++ b/test/Options/MailOptionsTest.php @@ -31,8 +31,8 @@ public function testGettersAndSetters(): void $this->assertSame(EsmtpTransport::class, $subject->getTransport()); $this->assertSame($transportMap, $subject->getTransportMap()); - $this->assertInstanceOf(MessageOptions::class, $subject->getMessageOptions()); - $this->assertInstanceOf(SmtpOptions::class, $subject->getSmtpOptions()); + $this->assertContainsOnlyInstancesOf(MessageOptions::class, [$subject->getMessageOptions()]); + $this->assertContainsOnlyInstancesOf(SmtpOptions::class, [$subject->getSmtpOptions()]); $this->assertSame($eventListeners, $subject->getEventListeners()); } } diff --git a/test/Options/MessageOptionsTest.php b/test/Options/MessageOptionsTest.php index 1179384..7a68400 100644 --- a/test/Options/MessageOptionsTest.php +++ b/test/Options/MessageOptionsTest.php @@ -45,7 +45,7 @@ public function testGettersAndSetters(): void $this->assertSame($cc, $subject->getCc()); $this->assertSame($bcc, $subject->getBcc()); $this->assertSame($messageSubject, $subject->getSubject()); - $this->assertInstanceOf(BodyOptions::class, $subject->getBody()); - $this->assertInstanceOf(AttachmentsOptions::class, $subject->getAttachments()); + $this->assertContainsOnlyInstancesOf(BodyOptions::class, [$subject->getBody()]); + $this->assertContainsOnlyInstancesOf(AttachmentsOptions::class, [$subject->getAttachments()]); } } diff --git a/test/Service/MailServiceTest.php b/test/Service/MailServiceTest.php index b87c78a..db8c13d 100644 --- a/test/Service/MailServiceTest.php +++ b/test/Service/MailServiceTest.php @@ -81,12 +81,12 @@ public function testGettersAndSetters(): void public function testCreateMailEvent(): void { $defaultMailEvent = $this->mailService->createMailEvent(); - $this->assertInstanceOf(MailEvent::class, $defaultMailEvent); + $this->assertContainsOnlyInstancesOf(MailEvent::class, [$defaultMailEvent]); $this->assertSame(MailEvent::EVENT_MAIL_PRE_SEND, $defaultMailEvent->getName()); $result = new MailResult(); $mailEvent = $this->mailService->createMailEvent('testName', $result); - $this->assertInstanceOf(MailEvent::class, $mailEvent); + $this->assertContainsOnlyInstancesOf(MailEvent::class, [$mailEvent]); $this->assertSame('testName', $mailEvent->getName()); $this->assertSame(MailResult::DEFAULT_MESSAGE, $mailEvent->getResult()->getMessage()); }