AJenbo/agcms

View on GitHub
application/inc/Models/Newsletter.php

Summary

Maintainability
A
2 hrs
Test Coverage
C
70%
<?php

namespace App\Models;

use App\Exceptions\Exception;
use App\Exceptions\Handler as ExceptionHandler;
use App\Services\ConfigService;
use App\Services\DbService;
use App\Services\EmailService;
use App\Services\OrmService;
use App\Services\RenderService;
use Throwable;

class Newsletter extends AbstractEntity implements InterfaceRichText
{
    /**  Table name in database. */
    public const TABLE_NAME = 'newsmails';

    // Backed by DB

    /** @var string Sender email address */
    private string $from = '';

    /** @var string Email subject */
    private string $subject = '';

    /** @var string Body */
    private string $html = '';

    /** @var bool Has it been sent. */
    private bool $sent = false;

    /** @var string[] List of topics is covered. */
    private array $interests = [];

    public function __construct(array $data = [])
    {
        $interests = $data['interests'] ?? null;
        if (!is_array($interests)) {
            $interests = [];
        }

        $this->setFrom(strval($data['from'] ?? ''))
            ->setSubject(strval($data['subject'] ?? ''))
            ->setInterests($interests)
            ->setHtml(strval($data['html'] ?? ''))
            ->setSent(boolval($data['sent'] ?? false))
            ->setId(intOrNull($data['id'] ?? null));
    }

    /**
     * @return $this
     */
    public function setFrom(string $from): self
    {
        $this->from = $from;

        return $this;
    }

    public function getFrom(): string
    {
        return $this->from;
    }

    /**
     * @return $this
     */
    public function setSubject(string $subject): self
    {
        $this->subject = $subject;

        return $this;
    }

    public function getSubject(): string
    {
        return $this->subject;
    }

    /**
     * @return $this
     */
    public function setHtml(string $html): InterfaceRichText
    {
        $this->html = $html;

        return $this;
    }

    public function getHtml(): string
    {
        return $this->html;
    }

    /**
     * @return $this
     */
    public function setSent(bool $sent): self
    {
        $this->sent = $sent;

        return $this;
    }

    public function isSent(): bool
    {
        return $this->sent;
    }

    /**
     * Set newsletter interests.
     *
     * @param string[] $interests
     *
     * @return $this
     */
    public function setInterests(array $interests): self
    {
        $this->interests = $interests;

        return $this;
    }

    /**
     * Get interests.
     *
     * @return string[]
     */
    public function getInterests(): array
    {
        return $this->interests;
    }

    public static function mapFromDB(array $data): array
    {
        $interests = explode('<', $data['interests']);
        $interests = array_map('html_entity_decode', $interests);

        return [
            'id'         => $data['id'],
            'from'       => $data['from'],
            'subject'    => $data['subject'],
            'html'       => $data['text'],
            'sent'       => (bool)$data['sendt'],
            'interests'  => $interests,
        ];
    }

    // ORM related functions

    public function getDbArray(): array
    {
        $interests = array_map('htmlspecialchars', $this->interests);
        $interests = implode('<', $interests);

        $db = app(DbService::class);

        return [
            'from'         => $db->quote($this->from),
            'subject'      => $db->quote($this->subject),
            'text'         => $db->quote($this->html),
            'sendt'        => $db->quote((string)(int)$this->sent),
            'interests'    => $db->quote($interests),
        ];
    }

    /**
     * Count number of recipients for this newsletter.
     */
    public function countRecipients(): int
    {
        $db = app(DbService::class);

        $db->addLoadedTable('email');
        $emails = $db->fetchOne(
            "
            SELECT count(DISTINCT email) as 'count'
            FROM `email`
            WHERE `email` NOT LIKE '' AND `kartotek` = '1'
            " . $this->getContactFilterSQL()
        );

        return (int)$emails['count'];
    }

    /**
     * Get SQL for filtering contacts based on interests.
     */
    private function getContactFilterSQL(): string
    {
        $andWhere = '';
        if ($this->interests) {
            foreach ($this->interests as $interest) {
                if ($andWhere) {
                    $andWhere .= ' OR ';
                }
                $andWhere .= '`interests` LIKE \'';
                $andWhere .= $interest;
                $andWhere .= '\' OR `interests` LIKE \'';
                $andWhere .= $interest;
                $andWhere .= '<%\' OR `interests` LIKE \'%<';
                $andWhere .= $interest;
                $andWhere .= '\' OR `interests` LIKE \'%<';
                $andWhere .= $interest;
                $andWhere .= '<%\'';
            }
            $andWhere = ' AND (' . $andWhere . ')';
        }

        return $andWhere;
    }

    /**
     * Send the newsletter.
     *
     * @todo resend failed emails, save bcc
     *
     * @throws Exception
     */
    public function send(): void
    {
        if ($this->sent) {
            throw new Exception(_('The newsletter has already been sent.'));
        }

        $andWhere = $this->getContactFilterSQL();
        $contacts = app(OrmService::class)->getByQuery(
            Contact::class,
            'SELECT * FROM email WHERE email NOT LIKE \'\' AND `kartotek` = \'1\' ' . $andWhere . ' GROUP BY `email`'
        );

        // Split in to groups of 99 to avoid server limit on bcc
        $contactsGroups = [];
        foreach ($contacts as $x => $contact) {
            $contactsGroups[(int)floor($x / 99) + 1][] = $contact;
        }

        $data = [
            'siteName' => ConfigService::getString('site_name'),
            'css'      => file_get_contents(
                app()->basePath('/theme/' . ConfigService::getString('theme', 'default') . '/style/email.css')
            ),
            'body'     => str_replace(' href="/', ' href="' . ConfigService::getString('base_url') . '/', $this->html),
        ];
        $emailService = app(EmailService::class);
        $failedCount = 0;

        $render = app(RenderService::class);

        foreach ($contactsGroups as $bcc) {
            $email = new Email([
                'subject'          => $this->subject,
                'body'             => $render->render('email/newsletter', $data),
                'senderName'       => ConfigService::getString('site_name'),
                'senderAddress'    => $this->from,
                'recipientName'    => ConfigService::getString('site_name'),
                'recipientAddress' => $this->from,
            ]);

            try {
                $emailService->send($email, $bcc);
            } catch (Throwable $exception) {
                app(ExceptionHandler::class)->report($exception);
                $failedCount += count($bcc);
            }
        }
        if ($failedCount) {
            throw new Exception(sprintf(_('Email %d/%d failed to be sent.'), $failedCount, count($contacts)));
        }

        $this->sent = true;
        $this->save();
    }
}