diff --git a/.env.dist b/.env.dist new file mode 100644 index 00000000..03df0e91 --- /dev/null +++ b/.env.dist @@ -0,0 +1,96 @@ +# This file is a "template" of what your .env file should look like. +# Set variables here that may be different on each deployment target of the app, +# e.g. development, staging, production. +# +# On `composer install`/`composer update`, this file is copied to `.env` (unless +# it already exists) and PHPLIST_SECRET is replaced with a freshly generated value. +# +# https://symfony.com/doc/current/configuration.html#configuring-environment-variables-in-env-files + +PHPLIST_DATABASE_DRIVER=pdo_mysql +PHPLIST_DATABASE_PATH= +PHPLIST_DATABASE_HOST=127.0.0.1 +PHPLIST_DATABASE_PORT=3306 +PHPLIST_DATABASE_NAME=phplistdb +PHPLIST_DATABASE_USER=phplist +PHPLIST_DATABASE_PASSWORD=phplist +DATABASE_PREFIX=phplist_ +LIST_TABLE_PREFIX=listattr_ +PHPLIST_ADMIN_PASSWORD=admin + +APP_DEV_VERSION=0 +APP_DEV_EMAIL=dev@dev.com +APP_POWERED_BY_PHPLIST=0 +PREFERENCEPAGE_SHOW_PRIVATE_LISTS=0 + +API_BASE_URL=http://api.phplist.local/ +FRONT_END_BASE_URL=http://frontend.phplist.local + +PARALLER_USE_WITH_PHPLIST3=0 + +# Email configuration +MAILER_FROM=noreply@phplist.com +MAILER_DSN=null://null +CONFIRMATION_URL=http://api.phplist.local/api/v2/subscriber/confirm/ +SUBSCRIPTION_CONFIRMATION_URL=http://api.phplist.local/api/v2/subscription/confirm/ +PASSWORD_RESET_URL=https://example.com/reset/ +SHOW_UNSUBSCRIBELINK=1 + +# Bounce email settings +BOUNCE_EMAIL=bounce@phplist.com +BOUNCE_IMAP_PASS=bounce@phplist.com +BOUNCE_IMAP_HOST=imap.phplist.com +BOUNCE_IMAP_PORT=993 +BOUNCE_IMAP_ENCRYPTION=ssl +BOUNCE_IMAP_MAILBOX=/var/spool/mail/bounces +BOUNCE_IMAP_MAILBOX_NAME=INBOX,ONE_MORE +BOUNCE_IMAP_PROTOCOL=imap +BOUNCE_IMAP_UNSUBSCRIBE_THRESHOLD=5 +BOUNCE_IMAP_BLACKLIST_THRESHOLD=3 +BOUNCE_IMAP_PURGE=0 +BOUNCE_IMAP_PURGE_UNPROCESSED=0 + +# Messenger configuration for asynchronous processing +MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=true + +# A secret key that's used to generate certain security-related tokens +PHPLIST_SECRET=%s +VERIFY_SSL=1 + +APP_PHPLIST_ISP_CONF_PATH=/etc/phplist.conf + +# Message sending +MAILQUEUE_BATCH_SIZE=5 +MAILQUEUE_BATCH_PERIOD=5 +MAILQUEUE_THROTTLE=5 +MESSAGING_MAX_PROCESS_TIME=600 +MAX_MAILSIZE=209715200 +DEFAULT_MESSAGEAGE=691200 +USE_MANUAL_TEXT_PART=0 +MESSAGING_BLACKLIST_GRACE_TIME=600 +GOOGLE_SENDERID= +USE_AMAZONSES=0 +USE_PRECEDENCE_HEADER=0 +EMBEDEXTERNALIMAGES=0 +EMBEDUPLOADIMAGES=0 +EXTERNALIMAGE_MAXAGE=0 +EXTERNALIMAGE_TIMEOUT=30 +EXTERNALIMAGE_MAXSIZE=204800 +FORWARD_ALTERNATIVE_CONTENT=0 +EMAILTEXTCREDITS=0 +ALWAYS_ADD_USERTRACK=1 +SEND_LISTADMIN_COPY=0 + +FORWARD_EMAIL_PERIOD="1 minute" +FORWARD_EMAIL_COUNT=1 +FORWARD_PERSONAL_NOTE_SIZE=0 +FORWARD_FRIEND_COUNT_ATTRIBUTE= +KEEPFORWARDERATTRIBUTES=0 + +UPLOADIMAGES_DIR=uploadimages +PHPLIST_UPLOADS_MAX_SIZE=5M + +PUBLIC_SCHEMA=https +PHPLIST_ATTACHMENT_DOWNLOAD_URL=https://example.com/download/ +PHPLIST_ATTACHMENT_REPOSITORY_PATH=/tmp +MAX_AVATAR_SIZE=100000 diff --git a/.env.test.local.dist b/.env.test.local.dist new file mode 100644 index 00000000..c9992c34 --- /dev/null +++ b/.env.test.local.dist @@ -0,0 +1,9 @@ +# Optional: copy this file to ".env.test.local" to run tests against an in-memory SQLite +# database instead of MySQL, so no database server is needed for `vendor/bin/phpunit`. +# +# Note: this file is not loaded automatically by PHPUnit CLI runs (this project's ApplicationKernel +# does not read .env files on its own); either export these as real environment variables before +# running phpunit, or wire them up via your own bootstrap/CI step. + +PHPLIST_DATABASE_DRIVER=pdo_sqlite +PHPLIST_DATABASE_PATH=:memory: \ No newline at end of file diff --git a/.gitignore b/.gitignore index 25db886b..072e5252 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,9 @@ /composer.lock /config/bundles.yml /config/config_modules.yml -/config/parameters.yml +/.env +/.env.local +/.env.*.local /config/routing_modules.yml /nbproject /var/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0254484d..f6e2111f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,10 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Added - Graylog integration for centralized logging (#TBD) +- `symfony/dotenv` support: configuration values are now read from a `.env` file (generated from `.env.dist` on install/update), in addition to real environment variables (#TBD) ### Changed +- `config/parameters.yml.dist` no longer contains inline `env(VAR): default` fallbacks; defaults now live in `.env.dist` (#TBD) ### Deprecated diff --git a/PHPDOC.md b/PHPDOC.md index 00ec597f..2243e294 100644 --- a/PHPDOC.md +++ b/PHPDOC.md @@ -1,25 +1,27 @@ -# Class Documentation with PHPDoc +# Generating class documentation -We use [phpdoc](phpdoc.org) to automatically generate documentation for our annotated classes. +We use [phpDocumentor](https://phpdoc.org) to generate API docs from the docblocks on +our classes, properties, and methods. Output settings (title, output path) are defined +in [`phpdoc.xml`](phpdoc.xml); the generated docs are written to `docs/phpdocumentor/` +and are not committed to the repository. -So to be able to generate or update our class docs you would need to download and install `phpDocumentor` globally (for system wide use) as shown below: +## Install phpDocumentor -1. `cd ~` [*Optional : it's recommended to navigate to your home dir before downloading `phpDocumentor` as shown in step 2*] -2. `wget https://phpdoc.org/phpDocumentor.phar` -3. `chmod +x phpDocumentor.phar` -4. `mv phpDocumentor.phar /usr/local/bin/phpDocumentor` +phpDocumentor ships as a standalone `.phar`. Install it once, globally: -*Possibility : In case you don't want to install `phpDocumentor` globally you can skip step 4, however you would need to run `phpDocumentor` from whatever path it was installed in.* +```bash +wget https://phpdoc.org/phpDocumentor.phar -O /usr/local/bin/phpDocumentor +chmod +x /usr/local/bin/phpDocumentor +``` -*Tip : You might need to run step four as root on some systems. That is : `sudo mv phpDocumentor.phar /usr/local/bin/phpDocumentor`* +If you'd rather not install it globally, download the `.phar` anywhere and call it +by its full path in the steps below. -## Generate Docs +## Generate the docs -If you did install `phpDocumentor` globally as specified above then you can generate class docs as follows. -Run : `composer run-php-documentor` +```bash +composer run-php-documentor +``` - - -*Note : `composer generate docs` would only work if you installed `phpDocumentor` globally, if you did not run : `custom/path/phpDocumentor -d 'src,tests' -t docs/phpdoc` to generate docs* - -*Where `custom/path/` is the location where you downloaded `phpDocumentor`* +This runs `phpDocumentor -d 'src,tests'`, using the output path from `phpdoc.xml`. +Open `docs/phpdocumentor/index.html` in a browser to view the result. \ No newline at end of file diff --git a/README.md b/README.md index 2015718a..4b929df2 100755 --- a/README.md +++ b/README.md @@ -52,18 +52,19 @@ this code. ## Documentation -* [Class Docs](docs/phpdoc/) * [Class structure overview](docs/ClassStructure.md) -* [Graphic domain model](docs/DomainModel/DomainModel.svg) and [description of the domain entities](docs/DomainModel/Entities.md) -* [Mailer Transports](docs/mailer-transports.md) - How to use different email providers (Gmail, Amazon SES, Mailchimp, SendGrid) -* [Asynchronous Email Sending](docs/AsyncEmailSending.md) - How to use asynchronous email sending with Symfony Messenger +* [Domain model diagram](docs/DomainModel/DomainModel.svg) and [description of the domain entities](docs/DomainModel/Entities.md) +* [Mailer transports](docs/MailerTransports.md) - configuring Gmail, Amazon SES, Mailchimp, and SendGrid +* [Asynchronous email sending](docs/AsyncEmailSending.md) - queuing email delivery with Symfony Messenger +* [Graylog integration](docs/Graylog.md) - centralized log management +* [Generating class API docs](PHPDOC.md) - regenerating the phpDocumentor output ## Running the web server The phpList application is configured so that the built-in PHP web server can run in development and testing mode, while Apache can run in production mode. -Please first set the database credentials in `config/parameters.yml`. +Please first set the database credentials in `.env` (created from `.env.dist` on `composer install`/`composer update`). ### Development @@ -79,12 +80,6 @@ already in use, on the next free port after 8000). You can stop the server with CTRL + C. -#### Development and Documentation - -We use `phpDocumentor` to automatically generate documentation for classes. To make this process efficient and easier, you are required to properly "document" your `classes`,`properties`, `methods` ... by annotating them with [docblocks](https://docs.phpdoc.org/latest/guide/guides/docblocks.html). - -More about generating docs in [PHPDOC.md](PHPDOC.md) - ### Testing Create test db with name phplist in your mysql DB or uncomment sqlite part in config_test.yml file to use in memory DB for functional tests. @@ -200,31 +195,14 @@ To access the phpList data from a third-party application (i.e., not from a phpList module), please use the [REST API](https://github.com/phpList/rest-api). -## Email Configuration - -phpList supports multiple email transport providers through Symfony Mailer. The following transports are included: - -* Gmail -* Amazon SES -* Mailchimp Transactional (Mandrill) -* SendGrid - -For detailed configuration instructions, see the [Mailer Transports documentation](docs/mailer-transports.md). - -## Copyright - -phpList is copyright (C) 2000-2025 [phpList Ltd](https://www.phplist.com/). - +## Translations -### Translations -command to extract translation strings +To extract translation strings from the source into an XLIFF catalog: ```bash php bin/console translation:extract --force en --format=xlf ``` -```bash -vendor/bin/phpstan analyse -c phpstan.neon; -vendor/bin/phpmd src/ text config/PHPMD/rules.xml; -vendor/bin/phpcs --standard=config/PhpCodeSniffer/ --ignore=*/Migrations/* bin/ src/ tests/ public/; -``` +## Copyright + +phpList is copyright (C) 2000-2025 [phpList Ltd](https://www.phplist.com/). diff --git a/composer.json b/composer.json index 9c95fb23..a6fd8125 100644 --- a/composer.json +++ b/composer.json @@ -78,7 +78,6 @@ "symfony/lock": "^6.4", "webklex/php-imap": "^6.2", "ext-imap": "*", - "tatevikgr/rss-feed": "dev-main", "ext-pdo": "*", "ezyang/htmlpurifier": "^4.19", "ext-libxml": "*", @@ -87,12 +86,14 @@ "ext-fileinfo": "*", "setasign/fpdf": "^1.8", "phpdocumentor/reflection-docblock": "^5.2", - "guzzlehttp/guzzle": "^7.4.5" + "guzzlehttp/guzzle": "^7.4.5", + "symfony/dotenv": "^6.4", + "symfony/doctrine-messenger": "^6.4" }, "require-dev": { "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "^3.2.0", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^2.2", "nette/caching": "^3.0.0", "nikic/php-parser": "^4.19.1", "phpmd/phpmd": "^2.6.0", @@ -103,11 +104,13 @@ "symfony/http-foundation": "^6.4", "symfony/routing": "^6.4", "symfony/console": "^6.4", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "phpstan/phpstan-doctrine": "^2.0" }, "suggest": { - "phplist/web-frontend": "5.0.x-dev", - "phplist/rest-api": "5.0.x-dev" + "phplist/web-frontend": "dev-main", + "phplist/rest-api": "dev-main", + "tatevikgr/rss-feed": "dev-main" }, "autoload": { "psr-4": { @@ -127,7 +130,7 @@ "PhpList\\Core\\Composer\\ScriptHandler::createGeneralConfiguration", "PhpList\\Core\\Composer\\ScriptHandler::createBundleConfiguration", "PhpList\\Core\\Composer\\ScriptHandler::createRoutesConfiguration", - "PhpList\\Core\\Composer\\ScriptHandler::createParametersConfiguration", + "PhpList\\Core\\Composer\\ScriptHandler::createDotenvConfiguration", "php bin/console cache:clear", "php bin/console cache:warmup" ], diff --git a/config/config_test.yml b/config/config_test.yml index 36ce489c..fe97391a 100644 --- a/config/config_test.yml +++ b/config/config_test.yml @@ -11,9 +11,12 @@ framework: doctrine: dbal: -# driver: 'pdo_sqlite' -# memory: true - driver: 'pdo_mysql' + # Defaults to pdo_mysql via PHPLIST_DATABASE_DRIVER (see .env). To run tests against an + # in-memory SQLite database instead (no MySQL server needed), set in .env.test.local: + # PHPLIST_DATABASE_DRIVER=pdo_sqlite + # PHPLIST_DATABASE_PATH=:memory: + driver: '%database_driver%' + path: '%database_path%' host: '%database_host%' port: '%database_port%' dbname: 'phplist' diff --git a/config/doctrine_migrations.yml b/config/doctrine_migrations.yml index 97e3bd6f..1db93ec5 100644 --- a/config/doctrine_migrations.yml +++ b/config/doctrine_migrations.yml @@ -1,8 +1,7 @@ doctrine_migrations: migrations_paths: 'PhpList\Core\Migrations': '%kernel.project_dir%/src/Migrations' -# 'TatevikGr\RssBundle\RssFeedBundle\Migrations': '%kernel.project_dir%/vendor/tatevikgr/rss-bundle/src/RssFeedBundle/Migrations' - all_or_nothing: true + all_or_nothing: false organize_migrations: false custom_template: '%kernel.project_dir%/src/Migrations/_template_migration.php.tpl' storage: diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index 4193c501..93022618 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -1,8 +1,7 @@ # This file is the Symfony Messenger configuration for asynchronous processing framework: messenger: - # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. - # failure_transport: failed + failure_transport: failed transports: # https://symfony.com/doc/current/messenger.html#transport-configuration @@ -20,7 +19,7 @@ framework: multiplier: 2 max_delay: 0 - # failed: 'doctrine://default?queue_name=failed' + failed: 'doctrine://default?queue_name=failed' routing: # Route your messages to the transports @@ -28,7 +27,7 @@ framework: 'PhpList\Core\Domain\Messaging\Message\SubscriberConfirmationMessage': async_email 'PhpList\Core\Domain\Messaging\Message\SubscriptionConfirmationMessage': async_email 'PhpList\Core\Domain\Messaging\Message\PasswordResetMessage': async_email - 'PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessage': async_email - 'PhpList\Core\Domain\Messaging\Message\SyncCampaignProcessorMessage': sync + 'PhpList\Core\Domain\Messaging\Message\CampaignProcessor\CampaignProcessorMessage': async_email + 'PhpList\Core\Domain\Messaging\Message\CampaignProcessor\SyncCampaignProcessorMessage': sync 'PhpList\Core\Domain\Subscription\Message\DynamicTableMessage': sync diff --git a/config/parameters.yml b/config/parameters.yml new file mode 100644 index 00000000..aecc30ec --- /dev/null +++ b/config/parameters.yml @@ -0,0 +1,99 @@ +# This file is a "template" of what your parameters.yml file should look like +# Set parameters here that may be different on each deployment target of the app, e.g. development, staging, production. +# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration +# +# These variables are read from environment variables using the "env" construct. +# The environment variables themselves are defined in the ".env" file (see ".env.dist" for the template) +# and/or in the actual environment (e.g. Apache host configuration, command line). +parameters: + database_driver: '%env(PHPLIST_DATABASE_DRIVER)%' + database_path: '%env(PHPLIST_DATABASE_PATH)%' + database_host: '%env(PHPLIST_DATABASE_HOST)%' + database_port: '%env(PHPLIST_DATABASE_PORT)%' + database_name: '%env(PHPLIST_DATABASE_NAME)%' + database_user: '%env(PHPLIST_DATABASE_USER)%' + database_password: '%env(PHPLIST_DATABASE_PASSWORD)%' + database_prefix: '%env(DATABASE_PREFIX)%' + list_table_prefix: '%env(LIST_TABLE_PREFIX)%' + app.dev_version: '%env(APP_DEV_VERSION)%' + app.dev_email: '%env(APP_DEV_EMAIL)%' + app.powered_by_phplist: '%env(APP_POWERED_BY_PHPLIST)%' + app.preference_page_show_private_lists: '%env(PREFERENCEPAGE_SHOW_PRIVATE_LISTS)%' + + app.rest_api_base_url: '%env(API_BASE_URL)%/api/v2' + app.api_base_url: '%env(API_BASE_URL)%' + app.frontend_base_url: '%env(FRONT_END_BASE_URL)%' + + parallel_use_with_phplist3: '%env(PARALLER_USE_WITH_PHPLIST3)%' + + # Email configuration + app.mailer_from: '%env(MAILER_FROM)%' + app.mailer_dsn: '%env(MAILER_DSN)%' + app.confirmation_url: '%env(CONFIRMATION_URL)%' + app.subscription_confirmation_url: '%env(SUBSCRIPTION_CONFIRMATION_URL)%' + app.password_reset_url: '%env(PASSWORD_RESET_URL)%' + app.show_unsubscribe_link: '%env(SHOW_UNSUBSCRIBELINK)%' + + # bounce email settings + imap_bounce.email: '%env(BOUNCE_EMAIL)%' + imap_bounce.password: '%env(BOUNCE_IMAP_PASS)%' + imap_bounce.host: '%env(BOUNCE_IMAP_HOST)%' + imap_bounce.port: '%env(BOUNCE_IMAP_PORT)%' + imap_bounce.encryption: '%env(BOUNCE_IMAP_ENCRYPTION)%' + imap_bounce.mailbox: '%env(BOUNCE_IMAP_MAILBOX)%' + imap_bounce.mailbox_name: '%env(BOUNCE_IMAP_MAILBOX_NAME)%' + imap_bounce.protocol: '%env(BOUNCE_IMAP_PROTOCOL)%' + imap_bounce.unsubscribe_threshold: '%env(BOUNCE_IMAP_UNSUBSCRIBE_THRESHOLD)%' + imap_bounce.blacklist_threshold: '%env(BOUNCE_IMAP_BLACKLIST_THRESHOLD)%' + imap_bounce.purge: '%env(BOUNCE_IMAP_PURGE)%' + imap_bounce.purge_unprocessed: '%env(BOUNCE_IMAP_PURGE_UNPROCESSED)%' + + # Messenger configuration for asynchronous processing + app.messenger_transport_dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + + # A secret key that's used to generate certain security-related tokens + secret: '%env(PHPLIST_SECRET)%' + phplist.verify_ssl: '%env(VERIFY_SSL)%' + + graylog_host: 'graylog.phplist.local' + graylog_port: 12201 + + app.phplist_isp_conf_path: '%env(APP_PHPLIST_ISP_CONF_PATH)%' + + # Message sending + messaging.mail_queue_batch_size: '%env(MAILQUEUE_BATCH_SIZE)%' + messaging.mail_queue_period: '%env(MAILQUEUE_BATCH_PERIOD)%' + messaging.mail_queue_throttle: '%env(MAILQUEUE_THROTTLE)%' + messaging.max_process_time: '%env(MESSAGING_MAX_PROCESS_TIME)%' + messaging.max_mail_size: '%env(MAX_MAILSIZE)%' + messaging.default_message_age: '%env(DEFAULT_MESSAGEAGE)%' + messaging.use_manual_text_part: '%env(USE_MANUAL_TEXT_PART)%' + messaging.blacklist_grace_time: '%env(MESSAGING_BLACKLIST_GRACE_TIME)%' + messaging.google_sender_id: '%env(GOOGLE_SENDERID)%' + messaging.use_amazon_ses: '%env(USE_AMAZONSES)%' + messaging.use_precedence_header: '%env(USE_PRECEDENCE_HEADER)%' + messaging.embed_external_images: '%env(EMBEDEXTERNALIMAGES)%' + messaging.embed_uploaded_images: '%env(EMBEDUPLOADIMAGES)%' + messaging.external_image_max_age: '%env(EXTERNALIMAGE_MAXAGE)%' + messaging.external_image_timeout: '%env(EXTERNALIMAGE_TIMEOUT)%' + messaging.external_image_max_size: '%env(EXTERNALIMAGE_MAXSIZE)%' + messaging.forward_alternative_content: '%env(FORWARD_ALTERNATIVE_CONTENT)%' + messaging.email_text_credits: '%env(EMAILTEXTCREDITS)%' + messaging.always_add_user_track: '%env(ALWAYS_ADD_USERTRACK)%' + messaging.send_list_admin_copy: '%env(SEND_LISTADMIN_COPY)%' + + phplist.forward_email_period: '%env(FORWARD_EMAIL_PERIOD)%' + phplist.forward_email_count: '%env(FORWARD_EMAIL_COUNT)%' + phplist.forward_personal_note_size: '%env(FORWARD_PERSONAL_NOTE_SIZE)%' + phplist.forward_friend_count_attribute: '%env(FORWARD_FRIEND_COUNT_ATTRIBUTE)%' + phplist.keep_forwarded_attributes: '%env(KEEPFORWARDERATTRIBUTES)%' + + phplist.upload_images_dir: '%env(UPLOADIMAGES_DIR)%' + phplist.uploads.allowed_mime_types: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'] + phplist.uploads.allowed_extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'] + phplist.uploads.max_size: '%env(PHPLIST_UPLOADS_MAX_SIZE)%' + + phplist.public_schema: '%env(PUBLIC_SCHEMA)%' + phplist.attachment_download_url: '%env(PHPLIST_ATTACHMENT_DOWNLOAD_URL)%' + phplist.attachment_repository_path: '%env(PHPLIST_ATTACHMENT_REPOSITORY_PATH)%' + phplist.max_avatar_size: '%env(MAX_AVATAR_SIZE)%' diff --git a/config/parameters.yml.dist b/config/parameters.yml.dist deleted file mode 100644 index cf9a17e6..00000000 --- a/config/parameters.yml.dist +++ /dev/null @@ -1,168 +0,0 @@ -# This file is a "template" of what your parameters.yml file should look like -# Set parameters here that may be different on each deployment target of the app, e.g. development, staging, production. -# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration -# -# These variables are read from environment variables using the "env" construct. -# You can set environment variables in the Apache host configuration and also on the command line. -# If you cannot provide any environment variables, you can also set the variables in this file -# in the lines with "env(VARIABLE_NAME)". -parameters: - database_driver: '%%env(PHPLIST_DATABASE_DRIVER)%%' - env(PHPLIST_DATABASE_DRIVER): 'pdo_mysql' - database_path: '%%env(PHPLIST_DATABASE_PATH)%%' - env(PHPLIST_DATABASE_PATH): null - database_host: '%%env(PHPLIST_DATABASE_HOST)%%' - env(PHPLIST_DATABASE_HOST): '127.0.0.1' - database_port: '%%env(PHPLIST_DATABASE_PORT)%%' - env(PHPLIST_DATABASE_PORT): '3306' - database_name: '%%env(PHPLIST_DATABASE_NAME)%%' - env(PHPLIST_DATABASE_NAME): 'phplistdb' - database_user: '%%env(PHPLIST_DATABASE_USER)%%' - env(PHPLIST_DATABASE_USER): 'phplist' - database_password: '%%env(PHPLIST_DATABASE_PASSWORD)%%' - env(PHPLIST_DATABASE_PASSWORD): 'phplist' - database_prefix: '%%env(DATABASE_PREFIX)%%' - env(DATABASE_PREFIX): 'phplist_' - list_table_prefix: '%%env(LIST_TABLE_PREFIX)%%' - env(LIST_TABLE_PREFIX): 'listattr_' - app.dev_version: '%%env(APP_DEV_VERSION)%%' - env(APP_DEV_VERSION): '0' - app.dev_email: '%%env(APP_DEV_EMAIL)%%' - env(APP_DEV_EMAIL): 'dev@dev.com' - app.powered_by_phplist: '%%env(APP_POWERED_BY_PHPLIST)%%' - env(APP_POWERED_BY_PHPLIST): '0' - app.preference_page_show_private_lists: '%%env(PREFERENCEPAGE_SHOW_PRIVATE_LISTS)%%' - env(PREFERENCEPAGE_SHOW_PRIVATE_LISTS): '0' - app.rest_api_base_url: '%%env(REST_API_BASE_URL)%%' - env(REST_API_BASE_URL): 'http://api.phplist.local/api/v2' - api_base_url: '%%env(API_BASE_URL)%%' - env(API_BASE_URL): 'http://api.phplist.local/' - app.frontend_base_url: '%%env(FRONT_END_BASE_URL)%%' - env(FRONT_END_BASE_URL): 'http://frontend.phplist.local' - parallel_use_with_phplist3: '%%env(parallel_use_with_phplist3)%%' - env(parallel_use_with_phplist3): '0' - - # Email configuration - app.mailer_from: '%%env(MAILER_FROM)%%' - env(MAILER_FROM): 'noreply@phplist.com' - app.mailer_dsn: '%%env(MAILER_DSN)%%' - env(MAILER_DSN): 'null://null' # set local_domain on transport - app.confirmation_url: '%%env(CONFIRMATION_URL)%%' - env(CONFIRMATION_URL): 'http://api.phplist.local/api/v2/subscriber/confirm/' - app.subscription_confirmation_url: '%%env(SUBSCRIPTION_CONFIRMATION_URL)%%' - env(SUBSCRIPTION_CONFIRMATION_URL): 'http://api.phplist.local/api/v2/subscription/confirm/' - app.password_reset_url: '%%env(PASSWORD_RESET_URL)%%' - env(PASSWORD_RESET_URL): 'https://example.com/reset/' - app.show_unsubscribe_link: '%%env(SHOW_UNSUBSCRIBELINK)%%' - env(SHOW_UNSUBSCRIBELINK): '1' - - # bounce email settings - imap_bounce.email: '%%env(BOUNCE_EMAIL)%%' - env(BOUNCE_EMAIL): 'bounce@phplist.com' - imap_bounce.password: '%%env(BOUNCE_IMAP_PASS)%%' - env(BOUNCE_IMAP_PASS): 'bounce@phplist.com' - imap_bounce.host: '%%env(BOUNCE_IMAP_HOST)%%' - env(BOUNCE_IMAP_HOST): 'imap.phplist.com' - imap_bounce.port: '%%env(BOUNCE_IMAP_PORT)%%' - env(BOUNCE_IMAP_PORT): '993' - imap_bounce.encryption: '%%env(BOUNCE_IMAP_ENCRYPTION)%%' - env(BOUNCE_IMAP_ENCRYPTION): 'ssl' - imap_bounce.mailbox: '%%env(BOUNCE_IMAP_MAILBOX)%%' - env(BOUNCE_IMAP_MAILBOX): '/var/spool/mail/bounces' - imap_bounce.mailbox_name: '%%env(BOUNCE_IMAP_MAILBOX_NAME)%%' - env(BOUNCE_IMAP_MAILBOX_NAME): 'INBOX,ONE_MORE' - imap_bounce.protocol: '%%env(BOUNCE_IMAP_PROTOCOL)%%' - env(BOUNCE_IMAP_PROTOCOL): 'imap' - imap_bounce.unsubscribe_threshold: '%%env(BOUNCE_IMAP_UNSUBSCRIBE_THRESHOLD)%%' - env(BOUNCE_IMAP_UNSUBSCRIBE_THRESHOLD): '5' - imap_bounce.blacklist_threshold: '%%env(BOUNCE_IMAP_BLACKLIST_THRESHOLD)%%' - env(BOUNCE_IMAP_BLACKLIST_THRESHOLD): '3' - imap_bounce.purge: '%%env(BOUNCE_IMAP_PURGE)%%' - env(BOUNCE_IMAP_PURGE): '0' - imap_bounce.purge_unprocessed: '%%env(BOUNCE_IMAP_PURGE_UNPROCESSED)%%' - env(BOUNCE_IMAP_PURGE_UNPROCESSED): '0' - - # Messenger configuration for asynchronous processing - app.messenger_transport_dsn: '%%env(MESSENGER_TRANSPORT_DSN)%%' - env(MESSENGER_TRANSPORT_DSN): 'doctrine://default?auto_setup=true' - - # A secret key that's used to generate certain security-related tokens - secret: '%%env(PHPLIST_SECRET)%%' - env(PHPLIST_SECRET): %1$s - phplist.verify_ssl: '%%env(VERIFY_SSL)%%' - env(VERIFY_SSL): '1' - - graylog_host: 'graylog.phplist.local' - graylog_port: 12201 - - app.phplist_isp_conf_path: '%%env(APP_PHPLIST_ISP_CONF_PATH)%%' - env(APP_PHPLIST_ISP_CONF_PATH): '/etc/phplist.conf' - - # Message sending - messaging.mail_queue_batch_size: '%%env(MAILQUEUE_BATCH_SIZE)%%' - env(MAILQUEUE_BATCH_SIZE): '5' - messaging.mail_queue_period: '%%env(MAILQUEUE_BATCH_PERIOD)%%' - env(MAILQUEUE_BATCH_PERIOD): '5' - messaging.mail_queue_throttle: '%%env(MAILQUEUE_THROTTLE)%%' - env(MAILQUEUE_THROTTLE): '5' - messaging.max_process_time: '%%env(MESSAGING_MAX_PROCESS_TIME)%%' - env(MESSAGING_MAX_PROCESS_TIME): '600' - messaging.max_mail_size: '%%env(MAX_MAILSIZE)%%' - env(MAX_MAILSIZE): '209715200' - messaging.default_message_age: '%%env(DEFAULT_MESSAGEAGE)%%' - env(DEFAULT_MESSAGEAGE): '691200' - messaging.use_manual_text_part: '%%env(USE_MANUAL_TEXT_PART)%%' - env(USE_MANUAL_TEXT_PART): '0' - messaging.blacklist_grace_time: '%%env(MESSAGING_BLACKLIST_GRACE_TIME)%%' - env(MESSAGING_BLACKLIST_GRACE_TIME): '600' - messaging.google_sender_id: '%%env(GOOGLE_SENDERID)%%' - env(GOOGLE_SENDERID): '' - messaging.use_amazon_ses: '%%env(USE_AMAZONSES)%%' - env(USE_AMAZONSES): '0' - messaging.use_precedence_header: '%%env(USE_PRECEDENCE_HEADER)%%' - env(USE_PRECEDENCE_HEADER): '0' - messaging.embed_external_images: '%%env(EMBEDEXTERNALIMAGES)%%' - env(EMBEDEXTERNALIMAGES): '0' - messaging.embed_uploaded_images: '%%env(EMBEDUPLOADIMAGES)%%' - env(EMBEDUPLOADIMAGES): '0' - messaging.external_image_max_age: '%%env(EXTERNALIMAGE_MAXAGE)%%' - env(EXTERNALIMAGE_MAXAGE): '0' - messaging.external_image_timeout: '%%env(EXTERNALIMAGE_TIMEOUT)%%' - env(EXTERNALIMAGE_TIMEOUT): '30' - messaging.external_image_max_size: '%%env(EXTERNALIMAGE_MAXSIZE)%%' - env(EXTERNALIMAGE_MAXSIZE): '204800' - messaging.forward_alternative_content: '%%env(FORWARD_ALTERNATIVE_CONTENT)%%' - env(FORWARD_ALTERNATIVE_CONTENT): '0' - messaging.email_text_credits: '%%env(EMAILTEXTCREDITS)%%' - env(EMAILTEXTCREDITS): '0' - messaging.always_add_user_track: '%%env(ALWAYS_ADD_USERTRACK)%%' - env(ALWAYS_ADD_USERTRACK): '1' - messaging.send_list_admin_copy: '%%env(SEND_LISTADMIN_COPY)%%' - env(SEND_LISTADMIN_COPY): '0' - - phplist.forward_email_period: '%%env(FORWARD_EMAIL_PERIOD)%%' - env(FORWARD_EMAIL_PERIOD): '1 minute' - phplist.forward_email_count: '%%env(FORWARD_EMAIL_COUNT)%%' - env(FORWARD_EMAIL_COUNT): '1' - phplist.forward_personal_note_size: '%%env(FORWARD_PERSONAL_NOTE_SIZE)%%' - env(FORWARD_PERSONAL_NOTE_SIZE): '0' - phplist.forward_friend_count_attribute: '%%env(FORWARD_FRIEND_COUNT_ATTRIBUTE)%%' - env(FORWARD_FRIEND_COUNT_ATTRIBUTE): '' - phplist.keep_forwarded_attributes: '%%env(KEEPFORWARDERATTRIBUTES)%%' - env(KEEPFORWARDERATTRIBUTES): '0' - - phplist.upload_images_dir: '%%env(UPLOADIMAGES_DIR)%%' - env(UPLOADIMAGES_DIR): 'uploadimages' - phplist.uploads.allowed_mime_types: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'] - phplist.uploads.allowed_extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'] - phplist.uploads.max_size: '%%env(PHPLIST_UPLOADS_MAX_SIZE)%%' - env(PHPLIST_UPLOADS_MAX_SIZE): '5M' - - phplist.public_schema: '%%env(PUBLIC_SCHEMA)%%' - env(PUBLIC_SCHEMA): 'https' - phplist.attachment_download_url: '%%env(PHPLIST_ATTACHMENT_DOWNLOAD_URL)%%' - env(PHPLIST_ATTACHMENT_DOWNLOAD_URL): 'https://example.com/download/' - phplist.attachment_repository_path: '%%env(PHPLIST_ATTACHMENT_REPOSITORY_PATH)%%' - env(PHPLIST_ATTACHMENT_REPOSITORY_PATH): '/tmp' - phplist.max_avatar_size: '%%env(MAX_AVATAR_SIZE)%%' - env(MAX_AVATAR_SIZE): '100000' diff --git a/config/services.yml b/config/services.yml index 7c053ed9..31a2b6f1 100644 --- a/config/services.yml +++ b/config/services.yml @@ -10,10 +10,10 @@ services: PhpList\Core\Core\ApplicationStructure: public: true - PhpList\Core\Security\Authentication: + PhpList\Core\Domain\Identity\Service\Authentication: public: true - PhpList\Core\Security\HashGenerator: + PhpList\Core\Domain\Identity\Service\HashGenerator: public: true PhpList\Core\Routing\ExtraLoader: @@ -51,6 +51,10 @@ services: tags: - { name: 'doctrine.dbal.schema_filter', connection: 'default' } + PhpList\Core\Core\Doctrine\TablePrefixListener: + arguments: + $tablePrefix: '%database_prefix%' + HTMLPurifier_Config: class: HTMLPurifier_Config factory: [ 'HTMLPurifier_Config', 'createDefault' ] diff --git a/config/services/commands.yml b/config/services/commands.yml index 7e16ac87..d9305748 100644 --- a/config/services/commands.yml +++ b/config/services/commands.yml @@ -12,9 +12,6 @@ services: resource: '../../src/Domain/Identity/Command' tags: ['console.command'] - PhpList\Core\Bounce\Command\ProcessBouncesCommand: + PhpList\Core\Domain\Messaging\Command\ProcessBouncesCommand: arguments: $protocolProcessors: !tagged_iterator 'phplist.bounce_protocol_processor' - - TatevikGr\RssFeedBundle\Command\RssDispatchCommand: - tags: ['console.command'] diff --git a/config/services/managers.yml b/config/services/managers.yml index 7a306db5..936bf38f 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -8,8 +8,6 @@ services: resource: '../../src/Domain/**/Service/Manager/*' exclude: '../../src/Domain/**/Service/Manager/Builder/*' - PhpList\Core\Bounce\Service\Manager\BounceManager: ~ - Doctrine\DBAL\Schema\AbstractSchemaManager: factory: ['@doctrine.dbal.default_connection', 'createSchemaManager'] diff --git a/config/services/processor.yml b/config/services/processor.yml index 1e591bac..8ff38bf4 100644 --- a/config/services/processor.yml +++ b/config/services/processor.yml @@ -4,20 +4,20 @@ services: autoconfigure: true public: false - PhpList\Core\Bounce\Service\Processor\PopBounceProcessor: + PhpList\Core\Domain\Messaging\Service\Processor\PopBounceProcessor: arguments: $host: '%imap_bounce.host%' $port: '%imap_bounce.port%' $mailboxNames: '%imap_bounce.mailbox_name%' tags: ['phplist.bounce_protocol_processor'] - PhpList\Core\Bounce\Service\Processor\MboxBounceProcessor: + PhpList\Core\Domain\Messaging\Service\Processor\MboxBounceProcessor: tags: ['phplist.bounce_protocol_processor'] - PhpList\Core\Bounce\Service\Processor\AdvancedBounceRulesProcessor: ~ + PhpList\Core\Domain\Messaging\Service\Processor\AdvancedBounceRulesProcessor: ~ - PhpList\Core\Bounce\Service\Processor\UnidentifiedBounceReprocessor: ~ + PhpList\Core\Domain\Messaging\Service\Processor\UnidentifiedBounceReprocessor: ~ - PhpList\Core\Bounce\Service\Processor\BounceDataProcessor: ~ + PhpList\Core\Domain\Messaging\Service\Processor\BounceDataProcessor: ~ PhpList\Core\Domain\Subscription\Service\SubscribePagePlaceholderProcessor: ~ diff --git a/config/services/repositories.yml b/config/services/repositories.yml index 37b31c18..a0650b35 100644 --- a/config/services/repositories.yml +++ b/config/services/repositories.yml @@ -33,7 +33,7 @@ services: arguments: - PhpList\Core\Domain\Identity\Model\Administrator - Doctrine\ORM\Mapping\ClassMetadata\ClassMetadata - - PhpList\Core\Security\HashGenerator + - PhpList\Core\Domain\Identity\Service\HashGenerator PhpList\Core\Domain\Identity\Repository\AdminAttributeValueRepository: parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: diff --git a/config/services/resolvers.yml b/config/services/resolvers.yml index baa9d3b9..64c97389 100644 --- a/config/services/resolvers.yml +++ b/config/services/resolvers.yml @@ -22,7 +22,7 @@ services: autowire: true autoconfigure: true - PhpList\Core\Bounce\Service\BounceActionResolver: + PhpList\Core\Domain\Messaging\Service\BounceActionResolver: arguments: - !tagged_iterator { tag: 'phplist.bounce_action_handler' } diff --git a/config/services/services.yml b/config/services/services.yml index 31fc1a32..9e527c13 100644 --- a/config/services/services.yml +++ b/config/services/services.yml @@ -138,7 +138,7 @@ services: autowire: true autoconfigure: true - PhpList\Core\Bounce\Service\ConsecutiveBounceHandler: + PhpList\Core\Domain\Messaging\Service\ConsecutiveBounceHandler: autowire: true autoconfigure: true arguments: @@ -147,7 +147,7 @@ services: Webklex\PHPIMAP\ClientManager: ~ - PhpList\Core\Bounce\Service\WebklexImapClientFactory: + PhpList\Core\Domain\Messaging\Service\WebklexImapClientFactory: autowire: true autoconfigure: true arguments: @@ -164,34 +164,34 @@ services: $username: '%imap_bounce.email%' $password: '%imap_bounce.password%' - PhpList\Core\Bounce\Service\NativeBounceProcessingService: + PhpList\Core\Domain\Messaging\Service\NativeBounceProcessingService: autowire: true autoconfigure: true arguments: $purgeProcessed: '%imap_bounce.purge%' $purgeUnprocessed: '%imap_bounce.purge_unprocessed%' - PhpList\Core\Bounce\Service\WebklexBounceProcessingService: + PhpList\Core\Domain\Messaging\Service\WebklexBounceProcessingService: autowire: true autoconfigure: true arguments: $purgeProcessed: '%imap_bounce.purge%' $purgeUnprocessed: '%imap_bounce.purge_unprocessed%' - PhpList\Core\Bounce\Service\LockService: + PhpList\Core\Domain\Messaging\Service\LockService: autowire: true autoconfigure: true - PhpList\Core\Bounce\Service\SubscriberBlacklistService: + PhpList\Core\Domain\Messaging\Service\SubscriberBlacklistService: autowire: true autoconfigure: true - PhpList\Core\Bounce\Service\MessageParser: + PhpList\Core\Domain\Messaging\Service\MessageParser: autowire: true autoconfigure: true _instanceof: - PhpList\Core\Bounce\Service\Handler\BounceActionHandlerInterface: + PhpList\Core\Domain\Messaging\Service\Handler\BounceActionHandlerInterface: tags: - { name: 'phplist.bounce_action_handler' } diff --git a/docs/AsyncEmailSending.md b/docs/AsyncEmailSending.md index da4f247c..386eae49 100644 --- a/docs/AsyncEmailSending.md +++ b/docs/AsyncEmailSending.md @@ -64,10 +64,10 @@ You can test the email functionality using the built-in command: ```bash # Queue an email for asynchronous sending -bin/console app:send-test-email recipient@example.com +bin/console phplist:test-email recipient@example.com # Send an email synchronously (immediately) -bin/console app:send-test-email recipient@example.com --sync +bin/console phplist:test-email recipient@example.com --sync ``` ## Processing the Email Queue @@ -90,8 +90,14 @@ bin/console messenger:stats # View failed messages bin/console messenger:failed:show + +# Retry a failed message +bin/console messenger:failed:retry ``` +Failed messages are routed to the `failed` transport (a separate queue in the +same Doctrine table), configured in `config/packages/messenger.yaml`. + ## Troubleshooting If emails are not being sent: diff --git a/docs/ClassStructure.md b/docs/ClassStructure.md index 8b3d9516..1f586515 100644 --- a/docs/ClassStructure.md +++ b/docs/ClassStructure.md @@ -46,4 +46,3 @@ Security‑related concerns. Utilities to support tests. - Traits/: Reusable traits and helpers used in the test suite. - diff --git a/docs/DomainModel/Entities.md b/docs/DomainModel/Entities.md index 5b83323d..a434b9f7 100644 --- a/docs/DomainModel/Entities.md +++ b/docs/DomainModel/Entities.md @@ -1,5 +1,8 @@ # Domain Entities +Table names below use the default `DATABASE_PREFIX` (`phplist_`, set in `.env`). The +prefix is applied dynamically at runtime, so it can be changed per installation. + ## Identity Context ### Administrator @@ -13,11 +16,23 @@ Administrators are not subscribers. If administrators would like to subscribe to subscriber lists, they need to have a separate subscriber account. ### AdministratorAttribute -Table name: `phplist_adminattribute` or `phplist_admin_attribute` +Table name: `phplist_adminattribute` + +This is similar to a subscriber attribute: It defines a field for +administrators (name and ID only, not the value). These can then be used as +placeholders in campaigns. + +### AdministratorAttributeValue +Table name: `phplist_admin_attribute` + +The value of a particular **AdministratorAttribute** for a particular +**administrator**. -This is similar to a subscriber attribute: It allows you to have details of -administrators. These can then be used in campaigns. Basically, you can add -placeholders for administrator attributes in campaigns. +### AdministratorLogin +Table name: `phplist_admin_login` + +A record of a single login session for an **administrator**: source IP +address, session ID, and whether the session is still active. ### AdministratorPasswordRequest Table name: `phplist_admin_password_request` @@ -31,15 +46,14 @@ This table contains the API tokens for **administrators**. Those API tokens are used for access to the REST API. In the web frontend, they are also used for CSRF protection. - -## SubscriptionContext +## Subscription Context ### Attribute Table name: `phplist_user_attribute` An **attribute** is a field for subscribers. This entity does not -contain the values for this attribute for each individual subscribe, but -only the name of the attribute and an ID. +contain the values for this attribute for each individual subscriber, but +only the name of the attribute and an ID. ### AttributeValue Table name: `phplist_user_user_attribute` @@ -50,7 +64,7 @@ particular **subscriber**. ### SubscribePage Table name: `phplist_subscribepage` -*subscribePages** allow setting up a selection of subscriber lists, attributes +**SubscribePages** allow setting up a selection of subscriber lists, attributes and language, and some other settings to control the content for the page that can be used to subscribe to the system. As a result, you can e.g., have different pages per language, which allows you to translate all the content @@ -97,8 +111,6 @@ multiple subscriber lists, and a campaign can be sent to multiple subscriber lists, but this association ensures that a subscriber always only receives one copy of a campaign, regardless of other associations. -Should we use a named association for this? What should it be named? - ### SuppressionList Table name: `phplist_user_blacklist` @@ -113,21 +125,25 @@ Table name: `phplist_user_blacklist_data` This is some more additional info on a SuppressionList. - ## Messaging Context - ### Attachment Table name: `phplist_attachment` An attachment represents a file attached to exactly one **campaign**. ### Bounce -Table name: `phplist_boune` +Table name: `phplist_bounce` + +A recorded bounce message: the original bounce email's header and body, plus +a classification status and comment. ### BounceRegEx Table name: `phplist_bounceregex` +A regular expression used to classify **bounces** by matching their content, +with an associated action (e.g. unsubscribe the subscriber). + ### Campaign Table name: `phplist_message` @@ -137,7 +153,9 @@ potentially multiple subscriber lists). The campaign has been created by an **subscribers**. It is stored to which subscribers a campaign has been sent. ### CampaignBounce -Table name: `phplist_message_bounce` +Table name: `phplist_user_message_bounce` + +Links a **bounce** to the **subscriber** and **campaign** it resulted from. ### CampaignData Table name: `phplist_messagedata` @@ -147,7 +165,7 @@ Google tracking IDs, special relationships to **subscriber lists**, and alias titles. ### CampaignForward -Table name: `phplist_message_forward` +Table name: `phplist_user_message_forward` This tracks details of **campaigns** which were forwarded by a recipient **subscriber** to someone else via an email message. @@ -170,7 +188,6 @@ Table name: `phplist_templateimage` This contains images used in **templates**. The blob contains the image. - ## System Context ### Configuration @@ -208,7 +225,6 @@ time they were updated), [the MD5 for that](https://phplist.com/files/tlds-alpha-by-domain.txt.md5), etc. etc. - ## Tracking Context ### LinkTrackForward @@ -229,7 +245,6 @@ Table name: `phplist_linktrack_uml_click` When a **subscriber** clicks on a link in a message, this click will be recorded here. - ## Unused entities * LinkTrack, table name: `phplist_linktrack` diff --git a/docs/Graylog.md b/docs/Graylog.md index 0abbcb57..b5db0671 100644 --- a/docs/Graylog.md +++ b/docs/Graylog.md @@ -1,81 +1,50 @@ # Graylog Integration -This document explains how to use the Graylog integration in the phpList core application. +phpList can send logs to [Graylog](https://graylog.org/) over GELF (Graylog Extended +Log Format) using Monolog's `gelf` handler. The handler ships **disabled by default** +in both environments. -## Overview +## Enabling it -Graylog is a log management platform that collects, indexes, and analyzes log messages from various sources. The phpList core application is configured to send logs to Graylog using the GELF (Graylog Extended Log Format) protocol. - -## Configuration - -The Graylog integration is configured in the following files: - -- `config/config_prod.yml` - Production environment configuration -- `config/config_dev.yml` - Development environment configuration - -### Default Configuration - -By default, the application is configured to: - -- In production: Send logs of level "error" and above to Graylog -- In development: Send logs of all levels to Graylog - -The default configuration points to a placeholder Graylog server at `graylog.example.com:12201`. You need to update this to point to your actual Graylog server. - -### Updating the Graylog Server Details - -To update the Graylog server details, modify the following sections in the configuration files: - -In `config/config_prod.yml`: - -```yaml -graylog: - type: gelf - publisher: - hostname: graylog.example.com # Replace with your Graylog server hostname - port: 12201 # Default GELF UDP port - level: error # Only send errors and above to Graylog -``` - -In `config/config_dev.yml`: +1. In `config/config_prod.yml`, uncomment the `graylog` handler under `monolog.handlers`. + It sends `error`-level and above logs, using the `graylog_host` and `graylog_port` + parameters from `config/parameters.yml` (defaults: `graylog.phplist.local:12201`). +2. In `config/config_dev.yml`, uncomment the `graylog` handler to also log in + development. It sends every level except the `event` channel. +3. Update `graylog_host` and `graylog_port` in `config/parameters.yml` to point at + your Graylog server. ```yaml -graylog: - type: gelf - publisher: - hostname: graylog.example.com # Replace with your Graylog server hostname - port: 12201 # Default GELF UDP port - level: debug # Send all logs to Graylog in development - channels: ['!event'] +# config/parameters.yml +parameters: + graylog_host: 'graylog.example.com' + graylog_port: 12201 ``` -Replace `graylog.example.com` with the hostname or IP address of your Graylog server, and update the port if necessary. +## Graylog server setup -## Graylog Server Setup +Your Graylog server needs a GELF UDP input to receive these logs: -To receive logs from the application, your Graylog server needs to be configured with a GELF UDP input: - -1. In the Graylog web interface, go to System > Inputs -2. Select "GELF UDP" from the dropdown and click "Launch new input" -3. Configure the input with the following settings: +1. In the Graylog web interface, go to System > Inputs. +2. Select "GELF UDP" and click "Launch new input". +3. Configure it with: - Title: phpList Core - - Bind address: 0.0.0.0 (to listen on all interfaces) - - Port: 12201 (or the port you specified in the configuration) -4. Click "Save" - -## Testing the Integration + - Bind address: `0.0.0.0` (listen on all interfaces) + - Port: `12201` (or whatever you set as `graylog_port`) +4. Click "Save". -To test if logs are being sent to Graylog: +## Testing the integration -1. Generate some log messages in the application (e.g., by triggering an error) -2. Check the Graylog web interface to see if the logs are being received -3. If logs are not appearing, check the application logs for any errors related to the Graylog connection +1. Trigger a log message in the application (e.g. an error). +2. Check the Graylog web interface for the message. +3. If nothing shows up, see Troubleshooting below. ## Troubleshooting -If logs are not appearing in Graylog: +If logs aren't appearing in Graylog: -1. Verify that the Graylog server is running and accessible from the application server -2. Check that the GELF UDP input is properly configured and running in Graylog -3. Ensure that there are no firewall rules blocking UDP traffic on port 12201 (or your configured port) -4. Check the application logs for any errors related to the Graylog connection +1. Confirm the `graylog` handler is uncommented in the config for the environment + you're testing. +2. Verify the Graylog server is running and reachable from the application server. +3. Check that the GELF UDP input is running and bound to the port you configured. +4. Check for firewall rules blocking UDP traffic on that port. \ No newline at end of file diff --git a/docs/MailerTransports.md b/docs/MailerTransports.md index cde763da..9488923a 100644 --- a/docs/MailerTransports.md +++ b/docs/MailerTransports.md @@ -80,7 +80,7 @@ Notes: After setting up your preferred mailer transport, you can test it using the built-in test command: ```bash -bin/console app:send-test-email recipient@example.com +bin/console phplist:test-email recipient@example.com ``` ## Switching Between Transports @@ -91,7 +91,7 @@ You can easily switch between different mailer transports by changing the `MAILE 2. Set the environment variable in your server configuration 3. Set the environment variable before running a command: ```bash - MAILER_DSN=sendgrid://API_KEY@default bin/console app:send-test-email recipient@example.com + MAILER_DSN=sendgrid://API_KEY@default bin/console phplist:test-email recipient@example.com ``` ## Additional Configuration diff --git a/phpstan.neon b/phpstan.neon index 3a51f9ec..70705d26 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,7 @@ +includes: + - vendor/phpstan/phpstan-doctrine/extension.neon + - vendor/phpstan/phpstan-doctrine/rules.neon + parameters: level: 5 paths: diff --git a/public/app.php b/public/app.php deleted file mode 100644 index 8e58c4f4..00000000 --- a/public/app.php +++ /dev/null @@ -1,11 +0,0 @@ -configure() - ->dispatch(); diff --git a/public/app_dev.php b/public/app_dev.php deleted file mode 100644 index 46c49194..00000000 --- a/public/app_dev.php +++ /dev/null @@ -1,14 +0,0 @@ -ensureDevelopmentOrTestingEnvironment() - ->setEnvironment(Environment::DEVELOPMENT) - ->configure() - ->dispatch(); diff --git a/public/app_test.php b/public/app_test.php deleted file mode 100644 index af816b87..00000000 --- a/public/app_test.php +++ /dev/null @@ -1,14 +0,0 @@ -ensureDevelopmentOrTestingEnvironment() - ->setEnvironment(Environment::TESTING) - ->configure() - ->dispatch(); diff --git a/src/Composer/ScriptHandler.php b/src/Composer/ScriptHandler.php index 55e23739..426ac71c 100644 --- a/src/Composer/ScriptHandler.php +++ b/src/Composer/ScriptHandler.php @@ -36,17 +36,22 @@ class ScriptHandler /** * @var string */ - const PARAMETERS_CONFIGURATION_FILE = '/config/parameters.yml'; + const GENERAL_CONFIGURATION_FILE = '/config/config_modules.yml'; /** * @var string */ - const GENERAL_CONFIGURATION_FILE = '/config/config_modules.yml'; + const DOTENV_FILE = '/.env'; + + /** + * @var string + */ + const DOTENV_TEMPLATE_FILE = '/.env.dist'; /** * @var string */ - const PARAMETERS_TEMPLATE_FILE = '/config/parameters.yml.dist'; + const PARAMETERS_CONFIGURATION_FILE = '/config/parameters.yml'; /** * @return string absolute application root directory without the trailing slash @@ -265,23 +270,40 @@ public static function clearAllCaches():void } /** - * Creates config/parameters.yml (the parameters configuration file). + * Creates the .env file (the environment variables consumed by the parameters configuration) + * by copying it from .env.dist, generating a fresh app secret in the process. * * @return void */ - public static function createParametersConfiguration(): void + public static function createDotenvConfiguration(): void { - $configurationFilePath = self::getApplicationRoot() . self::PARAMETERS_CONFIGURATION_FILE; - if (file_exists($configurationFilePath)) { + $appDotenvFilePath = self::getApplicationRoot() . self::DOTENV_FILE; + $templateFilePath = __DIR__ . '/../..' . static::DOTENV_TEMPLATE_FILE; + + if (file_exists($appDotenvFilePath)) { return; } - $templateFilePath = __DIR__ . '/../..' . static::PARAMETERS_TEMPLATE_FILE; $template = file_get_contents($templateFilePath); $secret = bin2hex(random_bytes(20)); $configuration = sprintf($template, $secret); + self::createAndWriteFile($appDotenvFilePath, $configuration); + } + + + /** + * Creates config/parameters.yml (the parameters configuration file). + * + * @return void + */ + public static function createParametersConfiguration(): void + { + $configurationFilePath = self::getApplicationRoot() . self::PARAMETERS_CONFIGURATION_FILE; + $templateFilePath = __DIR__ . '/../..' . static::PARAMETERS_CONFIGURATION_FILE; + $configuration = file_get_contents($templateFilePath); + self::createAndWriteFile($configurationFilePath, $configuration); } diff --git a/src/Core/ApplicationKernel.php b/src/Core/ApplicationKernel.php index 8f43e62b..8f67de65 100644 --- a/src/Core/ApplicationKernel.php +++ b/src/Core/ApplicationKernel.php @@ -128,6 +128,11 @@ public function registerContainerConfiguration(LoaderInterface $loader): void if (file_exists($twigConfigFile)) { $loader->load($twigConfigFile); } + + $messengerConfigFile = $this->getApplicationDir() . '/config/packages/messenger.yaml'; + if (file_exists($messengerConfigFile)) { + $loader->load($messengerConfigFile); + } } /** diff --git a/src/Core/Bootstrap.php b/src/Core/Bootstrap.php index 82ddb28f..4c7af464 100644 --- a/src/Core/Bootstrap.php +++ b/src/Core/Bootstrap.php @@ -7,6 +7,7 @@ use Doctrine\ORM\EntityManagerInterface; use Exception; use RuntimeException; +use Symfony\Component\Dotenv\Dotenv; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -147,10 +148,50 @@ public function configure(): Bootstrap { $this->isConfigured = true; - return $this->configureDebugging() + return $this->loadEnvironmentVariables() + ->configureDebugging() ->configureApplicationKernel(); } + /** + * Loads environment variables from the application's ".env" files (if present) using Symfony Dotenv, + * following the standard ".env" -> ".env.local" -> ".env.$environment" -> ".env.$environment.local" cascade. + * + * ".env.dist" is a template only and must never be used to source real configuration: Symfony Dotenv + * would otherwise silently load it (with its literal placeholder values) whenever ".env" is missing. + * + * @return Bootstrap fluent interface + * + * @throws RuntimeException if ".env" does not exist, or PHPLIST_SECRET was not resolved to a real value + * @SuppressWarnings("PHPMD.Superglobals") + */ + private function loadEnvironmentVariables(): Bootstrap + { + $applicationRoot = $this->applicationStructure->getApplicationRoot(); + $dotenvPath = $applicationRoot . '/.env'; + if (!file_exists($dotenvPath)) { + throw new RuntimeException( + 'No ".env" file was found at "' . $dotenvPath . '". Run "composer install"/"composer update" ' . + 'to generate it from ".env.dist" (which is a template only and must not be used directly), ' . + 'or create ".env" manually with a real PHPLIST_SECRET.', + 1754766600 + ); + } + + (new Dotenv())->loadEnv($dotenvPath, 'APP_ENV', $this->environment); + + $secret = $_SERVER['PHPLIST_SECRET'] ?? $_ENV['PHPLIST_SECRET'] ?? ''; + if ($secret === '' || $secret === '%s') { + throw new RuntimeException( + 'PHPLIST_SECRET in ".env" is missing or still set to the ".env.dist" template placeholder. ' . + 'Set it to a real, unique, freshly generated secret before starting the application.', + 1754766601 + ); + } + + return $this; + } + /** * Makes sure that configure has been called before. * diff --git a/src/Core/BounceProcessorPass.php b/src/Core/BounceProcessorPass.php index 6ec27dae..2ab5c9c5 100644 --- a/src/Core/BounceProcessorPass.php +++ b/src/Core/BounceProcessorPass.php @@ -4,9 +4,9 @@ namespace PhpList\Core\Core; -use PhpList\Core\Bounce\Service\BounceProcessingServiceInterface; -use PhpList\Core\Bounce\Service\NativeBounceProcessingService; -use PhpList\Core\Bounce\Service\WebklexBounceProcessingService; +use PhpList\Core\Domain\Messaging\Service\BounceProcessingServiceInterface; +use PhpList\Core\Domain\Messaging\Service\NativeBounceProcessingService; +use PhpList\Core\Domain\Messaging\Service\WebklexBounceProcessingService; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; diff --git a/src/Core/Doctrine/TablePrefixListener.php b/src/Core/Doctrine/TablePrefixListener.php new file mode 100644 index 00000000..4aa49acc --- /dev/null +++ b/src/Core/Doctrine/TablePrefixListener.php @@ -0,0 +1,52 @@ +getClassMetadata(); + + if ($metadata->isMappedSuperclass || $metadata->isEmbeddedClass) { + return; + } + + $isPrefixed = false; + foreach (self::PREFIXED_NAMESPACES as $namespace) { + if (str_starts_with($metadata->getName(), $namespace)) { + $isPrefixed = true; + break; + } + } + + if (!$isPrefixed) { + return; + } + + $metadata->setPrimaryTable([ + 'name' => $this->tablePrefix . $metadata->getTableName(), + ]); + } +} diff --git a/src/Domain/Analytics/Model/LinkTrack.php b/src/Domain/Analytics/Model/LinkTrack.php index 848dde5e..ef8d147b 100644 --- a/src/Domain/Analytics/Model/LinkTrack.php +++ b/src/Domain/Analytics/Model/LinkTrack.php @@ -12,12 +12,13 @@ use PhpList\Core\Domain\Common\Model\Interfaces\Identity; #[ORM\Entity(repositoryClass: LinkTrackRepository::class)] -#[ORM\Table(name: 'phplist_linktrack')] +#[ORM\Table(name: 'linktrack')] #[ORM\UniqueConstraint(name: 'phplist_linktrack_miduidurlindex', columns: ['messageid', 'userid', 'url'])] #[ORM\Index(name: 'phplist_linktrack_midindex', columns: ['messageid'])] #[ORM\Index(name: 'phplist_linktrack_miduidindex', columns: ['messageid', 'userid'])] #[ORM\Index(name: 'phplist_linktrack_uidindex', columns: ['userid'])] #[ORM\Index(name: 'phplist_linktrack_urlindex', columns: ['url'])] +#[ORM\Index(name: 'phplist_linktrack_latestclickindex', columns: ['latestclick'])] class LinkTrack implements DomainModel, Identity { #[ORM\Id] @@ -41,10 +42,10 @@ class LinkTrack implements DomainModel, Identity private ?DateTimeInterface $firstClick = null; #[ORM\Column(name: 'latestclick', type: 'datetime')] - private ?DateTimeInterface $latestClick = null; + private DateTimeInterface $latestClick; #[ORM\Column(type: 'integer', nullable: true, options: ['default' => 0])] - private int $clicked = 0; + private ?int $clicked = 0; public function __construct() { @@ -111,23 +112,23 @@ public function setFirstClick(?DateTimeInterface $firstClick): self return $this; } - public function getLatestClick(): ?DateTimeInterface + public function getLatestClick(): DateTimeInterface { return $this->latestClick; } - public function setLatestClick(?DateTimeInterface $latestClick): self + public function setLatestClick(DateTimeInterface $latestClick): self { $this->latestClick = $latestClick; return $this; } - public function getClicked(): int + public function getClicked(): ?int { return $this->clicked; } - public function setClicked(int $clicked): self + public function setClicked(?int $clicked): self { $this->clicked = $clicked; return $this; diff --git a/src/Domain/Analytics/Model/LinkTrackForward.php b/src/Domain/Analytics/Model/LinkTrackForward.php index 0e03c017..c2cf7a27 100644 --- a/src/Domain/Analytics/Model/LinkTrackForward.php +++ b/src/Domain/Analytics/Model/LinkTrackForward.php @@ -10,7 +10,7 @@ use PhpList\Core\Domain\Common\Model\Interfaces\Identity; #[ORM\Entity(repositoryClass: LinkTrackForwardRepository::class)] -#[ORM\Table(name: 'phplist_linktrack_forward')] +#[ORM\Table(name: 'linktrack_forward')] #[ORM\UniqueConstraint(name: 'phplist_linktrack_forward_urlunique', columns: ['urlhash'])] #[ORM\Index(name: 'phplist_linktrack_forward_urlindex', columns: ['url'])] #[ORM\Index(name: 'phplist_linktrack_forward_uuididx', columns: ['uuid'])] @@ -33,7 +33,7 @@ class LinkTrackForward implements DomainModel, Identity private ?string $uuid = ''; #[ORM\Column(type: 'boolean', nullable: true, options: ['default' => 0])] - private bool $personalise = false; + private ?bool $personalise = false; public function getId(): ?int { @@ -73,12 +73,12 @@ public function setUuid(?string $uuid): self return $this; } - public function isPersonalise(): bool + public function isPersonalise(): ?bool { return $this->personalise; } - public function setPersonalise(bool $personalise): self + public function setPersonalise(?bool $personalise): self { $this->personalise = $personalise; return $this; diff --git a/src/Domain/Analytics/Model/LinkTrackMl.php b/src/Domain/Analytics/Model/LinkTrackMl.php index 419c7911..ff6bab0a 100644 --- a/src/Domain/Analytics/Model/LinkTrackMl.php +++ b/src/Domain/Analytics/Model/LinkTrackMl.php @@ -10,7 +10,7 @@ use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; #[ORM\Entity(repositoryClass: LinkTrackMlRepository::class)] -#[ORM\Table(name: 'phplist_linktrack_ml')] +#[ORM\Table(name: 'linktrack_ml')] #[ORM\Index(name: 'phplist_linktrack_ml_fwdindex', columns: ['forwardid'])] #[ORM\Index(name: 'phplist_linktrack_ml_midindex', columns: ['messageid'])] class LinkTrackMl implements DomainModel diff --git a/src/Domain/Analytics/Model/LinkTrackUmlClick.php b/src/Domain/Analytics/Model/LinkTrackUmlClick.php index 3faf811d..93a4b487 100644 --- a/src/Domain/Analytics/Model/LinkTrackUmlClick.php +++ b/src/Domain/Analytics/Model/LinkTrackUmlClick.php @@ -11,7 +11,7 @@ use PhpList\Core\Domain\Common\Model\Interfaces\Identity; #[ORM\Entity(repositoryClass: LinkTrackUmlClickRepository::class)] -#[ORM\Table(name: 'phplist_linktrack_uml_click')] +#[ORM\Table(name: 'linktrack_uml_click')] #[ORM\UniqueConstraint(name: 'phplist_linktrack_uml_click_miduidfwdid', columns: ['messageid', 'userid', 'forwardid'])] #[ORM\Index(name: 'phplist_linktrack_uml_click_midindex', columns: ['messageid'])] #[ORM\Index(name: 'phplist_linktrack_uml_click_miduidindex', columns: ['messageid', 'userid'])] diff --git a/src/Domain/Analytics/Model/LinkTrackUserClick.php b/src/Domain/Analytics/Model/LinkTrackUserClick.php index 27205cbb..3725cf15 100644 --- a/src/Domain/Analytics/Model/LinkTrackUserClick.php +++ b/src/Domain/Analytics/Model/LinkTrackUserClick.php @@ -10,7 +10,7 @@ use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; #[ORM\Entity(repositoryClass: LinkTrackUserClickRepository::class)] -#[ORM\Table(name: 'phplist_linktrack_userclick')] +#[ORM\Table(name: 'linktrack_userclick')] #[ORM\Index(name: 'phplist_linktrack_userclick_linkindex', columns: ['linkid'])] #[ORM\Index(name: 'phplist_linktrack_userclick_linkuserindex', columns: ['linkid', 'userid'])] #[ORM\Index(name: 'phplist_linktrack_userclick_linkusermessageindex', columns: ['linkid', 'userid', 'messageid'])] diff --git a/src/Domain/Analytics/Model/UserMessageView.php b/src/Domain/Analytics/Model/UserMessageView.php index b391d3f3..66240fee 100644 --- a/src/Domain/Analytics/Model/UserMessageView.php +++ b/src/Domain/Analytics/Model/UserMessageView.php @@ -11,10 +11,11 @@ use PhpList\Core\Domain\Common\Model\Interfaces\Identity; #[ORM\Entity(repositoryClass: UserMessageViewRepository::class)] -#[ORM\Table(name: 'phplist_user_message_view')] +#[ORM\Table(name: 'user_message_view')] #[ORM\Index(name: 'phplist_user_message_view_msgidx', columns: ['messageid'])] #[ORM\Index(name: 'phplist_user_message_view_useridx', columns: ['userid'])] #[ORM\Index(name: 'phplist_user_message_view_usermsgidx', columns: ['userid', 'messageid'])] +// todo: #[ORM\Index(name: 'phplist_user_message_view_viewedidx', columns: ['viewed'])] class UserMessageView implements DomainModel, Identity { #[ORM\Id] diff --git a/src/Domain/Analytics/Model/UserStats.php b/src/Domain/Analytics/Model/UserStats.php index c7b4b97e..48789b3b 100644 --- a/src/Domain/Analytics/Model/UserStats.php +++ b/src/Domain/Analytics/Model/UserStats.php @@ -10,7 +10,7 @@ use PhpList\Core\Domain\Common\Model\Interfaces\Identity; #[ORM\Entity(repositoryClass: UserStatsRepository::class)] -#[ORM\Table(name: 'phplist_userstats')] +#[ORM\Table(name: 'userstats')] #[ORM\UniqueConstraint(name: 'phplist_userstats_entry', columns: ['unixdate', 'item', 'listid'])] #[ORM\Index(name: 'phplist_userstats_dateindex', columns: ['unixdate'])] #[ORM\Index(name: 'phplist_userstats_itemindex', columns: ['item'])] @@ -30,7 +30,7 @@ class UserStats implements DomainModel, Identity private ?string $item = null; #[ORM\Column(name: 'listid', type: 'integer', nullable: true, options: ['default' => 0])] - private int $listId = 0; + private ?int $listId = 0; #[ORM\Column(name: 'value', type: 'integer', nullable: true, options: ['default' => 0])] private ?int $value = null; @@ -50,7 +50,7 @@ public function getItem(): ?string return $this->item; } - public function getListId(): int + public function getListId(): ?int { return $this->listId; } @@ -72,7 +72,7 @@ public function setItem(?string $item): self return $this; } - public function setListId(int $listId): self + public function setListId(?int $listId): self { $this->listId = $listId; return $this; diff --git a/src/Domain/Analytics/Repository/LinkTrackRepository.php b/src/Domain/Analytics/Repository/LinkTrackRepository.php index 3d322099..4a66b17d 100644 --- a/src/Domain/Analytics/Repository/LinkTrackRepository.php +++ b/src/Domain/Analytics/Repository/LinkTrackRepository.php @@ -5,6 +5,7 @@ namespace PhpList\Core\Domain\Analytics\Repository; use DateTimeInterface; +use Doctrine\DBAL\Exception; use PhpList\Core\Domain\Analytics\Model\LinkTrack; use PhpList\Core\Domain\Common\Repository\AbstractRepository; use PhpList\Core\Domain\Common\Repository\CursorPaginationTrait; @@ -53,4 +54,56 @@ public function countBetween(DateTimeInterface $start, DateTimeInterface $end): ->getQuery() ->getSingleScalarResult(); } + + /** + * @return array counts keyed by 'Y-m-d' + * @throws Exception + */ + public function countGroupedByDay(DateTimeInterface $start, DateTimeInterface $end): array + { + $connection = $this->getEntityManager()->getConnection(); + $table = $this->getClassMetadata()->getTableName(); + + $sql = sprintf( + 'SELECT DATE(latestclick) AS day, COUNT(*) AS cnt FROM %s WHERE latestclick >= :start' + . ' AND latestclick <= :end GROUP BY DATE(latestclick)', + $table + ); + + $rows = $connection->executeQuery($sql, [ + 'start' => $start->format('Y-m-d H:i:s'), + 'end' => $end->format('Y-m-d H:i:s'), + ])->fetchAllAssociative(); + + $result = []; + foreach ($rows as $row) { + $result[(string) $row['day']] = (int) $row['cnt']; + } + return $result; + } + + /** + * @param int[] $messageIds + * @return array unique-clicker counts keyed by message id + */ + public function countUniqueClickersByMessageIds(array $messageIds): array + { + if (empty($messageIds)) { + return []; + } + + $rows = $this->createQueryBuilder('lt') + ->select('lt.messageId AS messageId, COUNT(DISTINCT lt.userId) AS cnt') + ->where('lt.messageId IN (:ids)') + ->setParameter('ids', $messageIds) + ->groupBy('lt.messageId') + ->getQuery() + ->getResult(); + + $result = []; + foreach ($rows as $row) { + $result[(int) $row['messageId']] = (int) $row['cnt']; + } + return $result; + } } diff --git a/src/Domain/Analytics/Repository/UserMessageViewRepository.php b/src/Domain/Analytics/Repository/UserMessageViewRepository.php index 5a08b569..2c232aa0 100644 --- a/src/Domain/Analytics/Repository/UserMessageViewRepository.php +++ b/src/Domain/Analytics/Repository/UserMessageViewRepository.php @@ -5,6 +5,7 @@ namespace PhpList\Core\Domain\Analytics\Repository; use DateTimeInterface; +use Doctrine\DBAL\Exception; use PhpList\Core\Domain\Common\Repository\AbstractRepository; use PhpList\Core\Domain\Common\Repository\CursorPaginationTrait; use PhpList\Core\Domain\Common\Repository\Interfaces\PaginatableRepositoryInterface; @@ -51,4 +52,56 @@ public function countBetween(DateTimeInterface $start, DateTimeInterface $end): ->getQuery() ->getSingleScalarResult(); } + + /** + * @return array counts keyed by 'Y-m-d' + * @throws Exception + */ + public function countGroupedByDay(DateTimeInterface $start, DateTimeInterface $end): array + { + $connection = $this->getEntityManager()->getConnection(); + $table = $this->getClassMetadata()->getTableName(); + + $sql = sprintf( + 'SELECT DATE(viewed) AS day, COUNT(*) AS cnt FROM %s WHERE viewed >= :start AND viewed <= :end' + . ' GROUP BY DATE(viewed)', + $table + ); + + $rows = $connection->executeQuery($sql, [ + 'start' => $start->format('Y-m-d H:i:s'), + 'end' => $end->format('Y-m-d H:i:s'), + ])->fetchAllAssociative(); + + $result = []; + foreach ($rows as $row) { + $result[(string) $row['day']] = (int) $row['cnt']; + } + return $result; + } + + /** + * @param int[] $messageIds + * @return array view counts keyed by message id + */ + public function countByMessageIds(array $messageIds): array + { + if (empty($messageIds)) { + return []; + } + + $rows = $this->createQueryBuilder('umv') + ->select('umv.messageId AS messageId, COUNT(umv.id) AS cnt') + ->where('umv.messageId IN (:ids)') + ->setParameter('ids', $messageIds) + ->groupBy('umv.messageId') + ->getQuery() + ->getResult(); + + $result = []; + foreach ($rows as $row) { + $result[(int) $row['messageId']] = (int) $row['cnt']; + } + return $result; + } } diff --git a/src/Domain/Analytics/Service/AnalyticsService.php b/src/Domain/Analytics/Service/AnalyticsService.php index 853c2f9e..e0ff6985 100644 --- a/src/Domain/Analytics/Service/AnalyticsService.php +++ b/src/Domain/Analytics/Service/AnalyticsService.php @@ -149,36 +149,12 @@ public function getViewOpensStatistics(int $limit = 50, int $lastId = 0): array */ public function getTopDomains(int $limit = 50, int $minSubscribers = 5): array { - $subscribers = $this->subscriberRepository->findAll(); + $rows = $this->subscriberRepository->getTopDomains($limit, $minSubscribers); - $domains = []; - foreach ($subscribers as $subscriber) { - $domain = $this->extractDomain($subscriber->getEmail()); - if ($domain !== '') { - $domains[$domain] = ($domains[$domain] ?? 0) + 1; - } - } - - $filteredDomains = array_filter($domains, function ($count) use ($minSubscribers) { - return $count >= $minSubscribers; - }); - - arsort($filteredDomains); - - $result = []; - $count = 0; - foreach ($filteredDomains as $domain => $subscriberCount) { - if ($count >= $limit) { - break; - } - - $result[] = [ - 'domain' => $domain, - 'subscribers' => $subscriberCount, - ]; - - $count++; - } + $result = array_map(static fn (array $row): array => [ + 'domain' => $row['domain'], + 'subscribers' => (int) $row['subscribers'], + ], $rows); return [ 'domains' => $result, @@ -281,69 +257,34 @@ private function calculateChange(float|int $current, float|int $previous): float */ public function getDomainConfirmationStatistics(int $limit = 50): array { - $domains = []; - $subscribers = $this->subscriberRepository->findAll(); - - foreach ($subscribers as $subscriber) { - $domain = $this->extractDomain($subscriber->getEmail()); - - if (!empty($domain)) { - if (!isset($domains[$domain])) { - $domains[$domain] = [ - 'confirmed' => 0, - 'unconfirmed' => 0, - 'blacklisted' => 0, - 'total' => 0, - ]; - } - - $domains[$domain]['total']++; - - if ($subscriber->isBlacklisted()) { - $domains[$domain]['blacklisted']++; - } elseif ($subscriber->isConfirmed()) { - $domains[$domain]['confirmed']++; - } else { - $domains[$domain]['unconfirmed']++; - } - } - } + $rows = $this->subscriberRepository->getDomainConfirmationStatistics($limit); - uasort($domains, function ($domain1, $domain2) { - return $domain2['unconfirmed'] <=> $domain1['unconfirmed']; - }); + $result = array_map(function (array $row): array { + $total = (int) $row['total']; + $confirmed = (int) $row['confirmed']; + $unconfirmed = (int) $row['unconfirmed']; + $blacklisted = (int) $row['blacklisted']; - $result = []; - $count = 0; - foreach ($domains as $domain => $stats) { - if ($count >= $limit) { - break; - } - - $domainTotal = $stats['total']; - - $result[] = [ - 'domain' => $domain, + return [ + 'domain' => $row['domain'], 'confirmed' => [ - 'count' => $stats['confirmed'], - 'percentage' => $this->formatStat($stats['confirmed'], $domainTotal) + 'count' => $confirmed, + 'percentage' => $this->formatStat($confirmed, $total) ], 'unconfirmed' => [ - 'count' => $stats['unconfirmed'], - 'percentage' => $this->formatStat($stats['unconfirmed'], $domainTotal) + 'count' => $unconfirmed, + 'percentage' => $this->formatStat($unconfirmed, $total) ], 'blacklisted' => [ - 'count' => $stats['blacklisted'], - 'percentage' => $this->formatStat($stats['blacklisted'], $domainTotal) + 'count' => $blacklisted, + 'percentage' => $this->formatStat($blacklisted, $total) ], 'total' => [ - 'count' => $stats['total'], - 'percentage' => $this->formatStat($stats['total'], $domainTotal) + 'count' => $total, + 'percentage' => $this->formatStat($total, $total) ], ]; - - $count++; - } + }, $rows); return [ 'domains' => $result, @@ -351,19 +292,6 @@ public function getDomainConfirmationStatistics(int $limit = 50): array ]; } - private function extractDomain(string $email): ?string - { - $atPoint = strrchr($email, '@'); - - if ($atPoint === false) { - return null; - } - - $domain = substr($atPoint, 1); - - return $domain !== '' ? $domain : null; - } - private function formatStat(int $count, int $total): int|float { $percentage = $total > 0 ? ($count / $total) * 100 : 0; @@ -384,43 +312,18 @@ private function formatStat(int $count, int $total): int|float */ public function getTopLocalParts(int $limit = 25): array { - $localParts = []; + $rows = $this->subscriberRepository->getTopLocalParts($limit); + $totalSubscribers = $this->subscriberRepository->countWithValidEmail(); - $subscribers = $this->subscriberRepository->findAll(); + $result = array_map(function (array $row) use ($totalSubscribers): array { + $count = (int) $row['count']; - foreach ($subscribers as $subscriber) { - $email = $subscriber->getEmail(); - $atPosition = strpos($email, '@'); - - if ($atPosition !== false) { - $localPart = substr($email, 0, $atPosition); - - if (!isset($localParts[$localPart])) { - $localParts[$localPart] = 0; - } - - $localParts[$localPart]++; - } - } - - arsort($localParts); - - $result = []; - $count = 0; - $totalSubscribers = array_sum($localParts); - foreach ($localParts as $localPart => $subscriberCount) { - if ($count >= $limit) { - break; - } - - $result[] = [ - 'localPart' => $localPart, - 'count' => $subscriberCount, - 'percentage' => $this->formatStat($subscriberCount, $totalSubscribers), + return [ + 'localPart' => $row['localPart'], + 'count' => $count, + 'percentage' => $this->formatStat($count, $totalSubscribers), ]; - - $count++; - } + }, $rows); return [ 'localParts' => $result, @@ -430,18 +333,21 @@ public function getTopLocalParts(int $limit = 25): array public function getCampaignPerformance(): array { - $performance = []; $endDate = new DateTimeImmutable('today 23:59:59'); $startDate = $endDate->sub(new DateInterval('P29D'))->modify('00:00:00'); + $opensByDay = $this->userMessageViewManager->countViewsGroupedByDay($startDate, $endDate); + $clicksByDay = $this->linkTrackManager->countClicksGroupedByDay($startDate, $endDate); + + $performance = []; for ($index = 0; $index < 30; $index++) { - $dayStart = $startDate->add(new DateInterval('P' . $index . 'D')); - $dayEnd = $dayStart->modify('23:59:59'); + $day = $startDate->add(new DateInterval('P' . $index . 'D')); + $dateKey = $day->format('Y-m-d'); $performance[] = [ - 'date' => $dayStart->format('Y-m-d'), - 'opens' => $this->userMessageViewManager->countViewsBetween($dayStart, $dayEnd), - 'clicks' => $this->linkTrackManager->countClicksBetween($dayStart, $dayEnd), + 'date' => $dateKey, + 'opens' => $opensByDay[$dateKey] ?? 0, + 'clicks' => $clicksByDay[$dateKey] ?? 0, ]; } @@ -459,16 +365,16 @@ public function getRecentCampaigns(int $limit = 5): array $messages = $this->messageRepository ->getFilteredAfterId((new MessageFilter())->setLastId(0)->setLimit($limit)) ->getItems(); + + $messageIds = array_map(static fn ($message) => $message->getId(), $messages); + $viewCounts = $this->userMessageViewManager->countViewsByMessageIds($messageIds); + $uniqueClickCounts = $this->linkTrackManager->countUniqueClickersByMessageIds($messageIds); + $recentCampaigns = []; foreach ($messages as $message) { - $views = $this->userMessageViewManager->countViewsByMessageId($message->getId()); - $linkTracks = $this->linkTrackManager->getLinkTracksByMessageId($message->getId()); - - $uniqueClickers = []; - foreach ($linkTracks as $linkTrack) { - $uniqueClickers[$linkTrack->getUserId()] = true; - } - $uniqueClicks = count($uniqueClickers); + $id = $message->getId(); + $views = $viewCounts[$id] ?? 0; + $uniqueClicks = $uniqueClickCounts[$id] ?? 0; $sentCount = $message->getMetadata()->getViews() + $message->getMetadata()->getBounceCount(); diff --git a/src/Domain/Analytics/Service/LinkTrackService.php b/src/Domain/Analytics/Service/LinkTrackService.php index d6d60f95..dc478e84 100644 --- a/src/Domain/Analytics/Service/LinkTrackService.php +++ b/src/Domain/Analytics/Service/LinkTrackService.php @@ -48,7 +48,7 @@ public function extractAndSaveLinks(MessagePrecacheDto $content, int $userId, ?i throw new MissingMessageIdException(); } - $links = $this->extractLinksFromHtml($content->content ?? ''); + $links = $this->extractLinksFromHtml($content->content); if ($content->htmlFooter) { $links = array_merge($links, $this->extractLinksFromHtml($content->htmlFooter)); diff --git a/src/Domain/Analytics/Service/Manager/LinkTrackManager.php b/src/Domain/Analytics/Service/Manager/LinkTrackManager.php index 9f657ebc..0775ec1d 100644 --- a/src/Domain/Analytics/Service/Manager/LinkTrackManager.php +++ b/src/Domain/Analytics/Service/Manager/LinkTrackManager.php @@ -34,4 +34,14 @@ public function countClicksBetween(DateTimeInterface $start, DateTimeInterface $ { return $this->linkTrackRepository->countBetween($start, $end); } + + public function countClicksGroupedByDay(DateTimeInterface $start, DateTimeInterface $end): array + { + return $this->linkTrackRepository->countGroupedByDay($start, $end); + } + + public function countUniqueClickersByMessageIds(array $messageIds): array + { + return $this->linkTrackRepository->countUniqueClickersByMessageIds($messageIds); + } } diff --git a/src/Domain/Analytics/Service/Manager/UserMessageViewManager.php b/src/Domain/Analytics/Service/Manager/UserMessageViewManager.php index 6dce3cf7..52192651 100644 --- a/src/Domain/Analytics/Service/Manager/UserMessageViewManager.php +++ b/src/Domain/Analytics/Service/Manager/UserMessageViewManager.php @@ -34,4 +34,14 @@ public function countViewsBetween(DateTimeInterface $start, DateTimeInterface $e { return $this->userMessageViewRepository->countBetween($start, $end); } + + public function countViewsGroupedByDay(DateTimeInterface $start, DateTimeInterface $end): array + { + return $this->userMessageViewRepository->countGroupedByDay($start, $end); + } + + public function countViewsByMessageIds(array $messageIds): array + { + return $this->userMessageViewRepository->countByMessageIds($messageIds); + } } diff --git a/src/Domain/Identity/Exception/AttributeDefinitionCreationException.php b/src/Domain/Common/Exception/AttributeDefinitionCreationException.php similarity index 89% rename from src/Domain/Identity/Exception/AttributeDefinitionCreationException.php rename to src/Domain/Common/Exception/AttributeDefinitionCreationException.php index 5d105893..af507cca 100644 --- a/src/Domain/Identity/Exception/AttributeDefinitionCreationException.php +++ b/src/Domain/Common/Exception/AttributeDefinitionCreationException.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Identity\Exception; +namespace PhpList\Core\Domain\Common\Exception; use RuntimeException; diff --git a/src/Domain/Common/PdfGenerator.php b/src/Domain/Common/PdfGenerator.php index 1f25840f..7b852aed 100644 --- a/src/Domain/Common/PdfGenerator.php +++ b/src/Domain/Common/PdfGenerator.php @@ -12,9 +12,7 @@ public function createPdfBytes(string $text): string { $pdf = new FPDF(); // Disable compression to ensure plain text and metadata are visible in output (helps testing) - if (method_exists($pdf, 'SetCompression')) { - $pdf->SetCompression(false); - } + $pdf->SetCompression(false); $pdf->SetCreator('phpList'); $pdf->AddPage(); $pdf->SetFont('Arial', '', 12); diff --git a/src/Domain/Common/Repository/AbstractRepository.php b/src/Domain/Common/Repository/AbstractRepository.php index bfefd054..aa77520b 100644 --- a/src/Domain/Common/Repository/AbstractRepository.php +++ b/src/Domain/Common/Repository/AbstractRepository.php @@ -11,7 +11,7 @@ * Base class for repositories. * * @author Oliver Klee - * @SuppressWarnings(PHPMD.NumberOfChildren) + * @SuppressWarnings("PHPMD.NumberOfChildren") */ abstract class AbstractRepository extends EntityRepository { diff --git a/src/Domain/Common/Service/ExternalImageService.php b/src/Domain/Common/Service/ExternalImageService.php index 08452a08..63d9f35c 100644 --- a/src/Domain/Common/Service/ExternalImageService.php +++ b/src/Domain/Common/Service/ExternalImageService.php @@ -121,7 +121,7 @@ private function downloadUsingCurl(string $filename): ?string if ($cURLHandle !== false) { curl_setopt($cURLHandle, CURLOPT_HTTPGET, true); - curl_setopt($cURLHandle, CURLOPT_HEADER, 0); + curl_setopt($cURLHandle, CURLOPT_HEADER, false); curl_setopt($cURLHandle, CURLOPT_RETURNTRANSFER, true); curl_setopt($cURLHandle, CURLOPT_TIMEOUT, $this->externalImageTimeout); curl_setopt($cURLHandle, CURLOPT_FOLLOWLOCATION, true); diff --git a/src/Domain/Common/Validator/AbstractAttributeTypeValidator.php b/src/Domain/Common/Validator/AbstractAttributeTypeValidator.php new file mode 100644 index 00000000..b6982705 --- /dev/null +++ b/src/Domain/Common/Validator/AbstractAttributeTypeValidator.php @@ -0,0 +1,72 @@ +normalizeToEnum($value); + + if (!in_array($enum, $this->getValidTypes(), true)) { + $validList = implode(', ', array_map( + static fn (AttributeTypeEnum $enum) => $enum->value, + $this->getValidTypes() + )); + + $message = $this->translator->trans( + 'Invalid attribute type: "%type%". Valid types are: %valid_types%', + [ + '%type%' => $enum->value, + '%valid_types%' => $validList, + ] + ); + + throw new ValidatorException($message); + } + } + + /** + * @throws ValidatorException if value cannot be converted to AttributeTypeEnum + */ + private function normalizeToEnum(mixed $value): AttributeTypeEnum + { + if ($value instanceof AttributeTypeEnum) { + return $value; + } + + if (is_string($value)) { + try { + return AttributeTypeEnum::from($value); + } catch (Throwable) { + $lower = strtolower($value); + foreach (AttributeTypeEnum::cases() as $case) { + if ($case->value === $lower) { + return $case; + } + } + } + } + + throw new ValidatorException( + $this->translator->trans('Value must be an AttributeTypeEnum or string.') + ); + } +} diff --git a/src/Domain/Common/Validator/UploadValidator.php b/src/Domain/Common/Validator/UploadValidator.php index d2602211..d5d810d3 100644 --- a/src/Domain/Common/Validator/UploadValidator.php +++ b/src/Domain/Common/Validator/UploadValidator.php @@ -107,11 +107,11 @@ private function parseSizeLimit(string $value): int $size = (int) $matches[1]; $unit = strtolower((string) ($matches[2] ?? '')); - return match ($unit) { - '', 'b' => $size, - 'k', 'kb' => $size * 1024, - 'm', 'mb' => $size * 1024 * 1024, - 'g', 'gb' => $size * 1024 * 1024 * 1024, + return match (true) { + $unit === '' || $unit === 'b' => $size, + str_starts_with($unit, 'k') => $size * 1024, + str_starts_with($unit, 'm') => $size * 1024 * 1024, + str_starts_with($unit, 'g') => $size * 1024 * 1024 * 1024, default => throw new InvalidUploadException(sprintf('Invalid upload size limit "%s".', $value)), }; } diff --git a/src/Domain/Configuration/Model/Config.php b/src/Domain/Configuration/Model/Config.php index 00f0a6c5..80f60f19 100644 --- a/src/Domain/Configuration/Model/Config.php +++ b/src/Domain/Configuration/Model/Config.php @@ -9,7 +9,7 @@ use PhpList\Core\Domain\Configuration\Repository\ConfigRepository; #[ORM\Entity(repositoryClass: ConfigRepository::class)] -#[ORM\Table(name: 'phplist_config')] +#[ORM\Table(name: 'config')] class Config implements DomainModel { #[ORM\Id] diff --git a/src/Domain/Configuration/Model/EventLog.php b/src/Domain/Configuration/Model/EventLog.php index c0cff22b..7e1ac3af 100644 --- a/src/Domain/Configuration/Model/EventLog.php +++ b/src/Domain/Configuration/Model/EventLog.php @@ -12,7 +12,7 @@ use PhpList\Core\Domain\Configuration\Repository\EventLogRepository; #[ORM\Entity(repositoryClass: EventLogRepository::class)] -#[ORM\Table(name: 'phplist_eventlog')] +#[ORM\Table(name: 'eventlog')] #[ORM\Index(name: 'phplist_eventlog_enteredidx', columns: ['entered'])] #[ORM\Index(name: 'phplist_eventlog_pageidx', columns: ['page'])] #[ORM\HasLifecycleCallbacks] diff --git a/src/Domain/Configuration/Model/I18n.php b/src/Domain/Configuration/Model/I18n.php deleted file mode 100644 index 72397bb4..00000000 --- a/src/Domain/Configuration/Model/I18n.php +++ /dev/null @@ -1,67 +0,0 @@ -lan; - } - - public function setLan(string $lan): self - { - $this->lan = $lan; - return $this; - } - - public function getOriginal(): string - { - return $this->original; - } - - public function setOriginal(string $original): self - { - $this->original = $original; - return $this; - } - - public function getTranslation(): string - { - return $this->translation; - } - - public function setTranslation(string $translation): self - { - $this->translation = $translation; - return $this; - } -} diff --git a/src/Domain/Configuration/Model/UrlCache.php b/src/Domain/Configuration/Model/UrlCache.php index b6d032b9..adcea26a 100644 --- a/src/Domain/Configuration/Model/UrlCache.php +++ b/src/Domain/Configuration/Model/UrlCache.php @@ -11,7 +11,7 @@ use PhpList\Core\Domain\Configuration\Repository\UrlCacheRepository; #[ORM\Entity(repositoryClass: UrlCacheRepository::class)] -#[ORM\Table(name: 'phplist_urlcache')] +#[ORM\Table(name: 'urlcache')] #[ORM\Index(name: 'phplist_urlcache_urlindex', columns: ['url'])] #[ORM\HasLifecycleCallbacks] class UrlCache implements DomainModel, Identity @@ -33,7 +33,7 @@ class UrlCache implements DomainModel, Identity private ?DateTime $added = null; #[ORM\Column(name: 'content', type: 'blob', nullable: true)] - private ?string $content = null; + private mixed $content = null; public function getId(): ?int { @@ -57,6 +57,11 @@ public function getAdded(): ?DateTime public function getContent(): ?string { + if (is_resource($this->content)) { + $value = stream_get_contents($this->content); + return $value === false ? null : $value; + } + return $this->content; } diff --git a/src/Domain/Configuration/Repository/I18nRepository.php b/src/Domain/Configuration/Repository/I18nRepository.php deleted file mode 100644 index 33fa599a..00000000 --- a/src/Domain/Configuration/Repository/I18nRepository.php +++ /dev/null @@ -1,12 +0,0 @@ -booleanValues, true)) { throw new InvalidArgumentException('Invalid boolean value key'); } - $config = $this->configRepository->findOneBy(['item' => $key->value]); + $config = $this->configRepository->findOneBy(['key' => $key->value]); if ($config !== null) { return filter_var($config->getValue(), FILTER_VALIDATE_BOOLEAN); diff --git a/src/Domain/Configuration/Service/Provider/DefaultConfigProvider.php b/src/Domain/Configuration/Service/Provider/DefaultConfigProvider.php index 51b95ea1..3222acc9 100644 --- a/src/Domain/Configuration/Service/Provider/DefaultConfigProvider.php +++ b/src/Domain/Configuration/Service/Provider/DefaultConfigProvider.php @@ -20,7 +20,7 @@ public function __construct(private TranslatorInterface $translator) { } - /** @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ + /** @SuppressWarnings("PHPMD.ExcessiveMethodLength") */ private function init(): void { if (!empty($this->defaults)) { diff --git a/src/Domain/Identity/Command/CreateAdminCommand.php b/src/Domain/Identity/Command/CreateAdminCommand.php new file mode 100644 index 00000000..c35aa061 --- /dev/null +++ b/src/Domain/Identity/Command/CreateAdminCommand.php @@ -0,0 +1,135 @@ +addOption('login', null, InputOption::VALUE_REQUIRED, 'Login name for the admin') + ->addOption('password', null, InputOption::VALUE_REQUIRED, 'Password for the admin') + ->addOption('email', null, InputOption::VALUE_REQUIRED, 'Email for the admin'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + + $login = $this->resolveValue($input, $output, $helper, 'login', 'Enter login for admin: ', false); + if ($login === null) { + $output->writeln('Login must not be empty.'); + return Command::FAILURE; + } + + $existing = $this->administratorRepository->findOneBy(['loginName' => $login]); + if ($existing !== null) { + $output->writeln(sprintf( + 'Admin already exists: login="%s", email="%s"', + $existing->getLoginName(), + $existing->getEmail(), + )); + return Command::SUCCESS; + } + + $email = $this->resolveValue($input, $output, $helper, 'email', 'Enter email for admin: ', false); + if ($email === null) { + $output->writeln('Email must not be empty.'); + return Command::FAILURE; + } + + $password = $this->resolveValue( + $input, + $output, + $helper, + 'password', + sprintf('Enter password for admin (login "%s"): ', $login), + true + ); + if ($password === null) { + $output->writeln('Password must not be empty.'); + return Command::FAILURE; + } + + $dto = new CreateAdministratorDto( + loginName: $login, + password: $password, + email: $email, + isSuperUser: true, + privileges: $this->allPrivilegesGranted(), + ); + $admin = $this->administratorManager->createAdministrator($dto); + $this->entityManager->flush(); + + $output->writeln(sprintf( + 'Admin created: login="%s", email="%s", superuser=yes, privileges=all', + $admin->getLoginName(), + $admin->getEmail() + )); + + return Command::SUCCESS; + } + + private function resolveValue( + InputInterface $input, + OutputInterface $output, + QuestionHelper $helper, + string $optionName, + string $prompt, + bool $hidden + ): ?string { + $value = $input->getOption($optionName); + + if ($value === null) { + $question = new Question($prompt); + if ($hidden) { + $question->setHidden(true); + $question->setHiddenFallback(false); + } + $value = $helper->ask($input, $output, $question); + } + + $value = (string) $value; + return trim($value) === '' ? null : $value; + } + + /** + * @return array + */ + private function allPrivilegesGranted(): array + { + $all = []; + foreach (PrivilegeFlag::cases() as $flag) { + $all[$flag->value] = true; + } + return $all; + } +} diff --git a/src/Domain/Identity/Command/ImportDefaultsCommand.php b/src/Domain/Identity/Command/ImportDefaultsCommand.php deleted file mode 100644 index 47ac4295..00000000 --- a/src/Domain/Identity/Command/ImportDefaultsCommand.php +++ /dev/null @@ -1,99 +0,0 @@ -allPrivilegesGranted(); - - $existing = $this->administratorRepository->findOneBy(['loginName' => $login]); - if ($existing === null) { - // If creating the default admin, require a password. Prefer env var, else prompt for input. - $password = $envPassword; - if ($password === null) { - /** @var QuestionHelper $helper */ - $helper = $this->getHelper('question'); - $question = new Question('Enter password for default admin (login "admin"): '); - $question->setHidden(true); - $question->setHiddenFallback(false); - $password = (string) $helper->ask($input, $output, $question); - if (trim($password) === '') { - $output->writeln('Password must not be empty.'); - return Command::FAILURE; - } - } - - $dto = new CreateAdministratorDto( - loginName: $login, - password: $password, - email: $email, - isSuperUser: true, - privileges: $allPrivileges, - ); - $admin = $this->administratorManager->createAdministrator($dto); - $this->entityManager->flush(); - - $output->writeln(sprintf( - 'Default admin created: login="%s", email="%s", superuser=yes, privileges=all', - $admin->getLoginName(), - $admin->getEmail() - )); - } else { - $output->writeln(sprintf( - 'Default admin already exists: login="%s", email="%s"', - $existing->getLoginName(), - $existing->getEmail(), - )); - } - - return Command::SUCCESS; - } - - /** - * @return array - */ - private function allPrivilegesGranted(): array - { - $all = []; - foreach (PrivilegeFlag::cases() as $flag) { - $all[$flag->value] = true; - } - return $all; - } -} diff --git a/src/Domain/Identity/Model/AdminAttributeDefinition.php b/src/Domain/Identity/Model/AdminAttributeDefinition.php index 3fe45e76..d1b62e58 100644 --- a/src/Domain/Identity/Model/AdminAttributeDefinition.php +++ b/src/Domain/Identity/Model/AdminAttributeDefinition.php @@ -12,7 +12,7 @@ use PhpList\Core\Domain\Identity\Repository\AdminAttributeDefinitionRepository; #[ORM\Entity(repositoryClass: AdminAttributeDefinitionRepository::class)] -#[ORM\Table(name: 'phplist_adminattribute')] +#[ORM\Table(name: 'adminattribute')] #[ORM\HasLifecycleCallbacks] class AdminAttributeDefinition implements DomainModel, Identity { @@ -39,6 +39,9 @@ class AdminAttributeDefinition implements DomainModel, Identity #[ORM\Column(name:'tablename', type: 'string', length: 255, nullable: true)] private ?string $tableName; + /** + * @var Collection + */ #[ORM\OneToMany( targetEntity: AdminAttributeValue::class, mappedBy: 'attributeDefinition', diff --git a/src/Domain/Identity/Model/AdminAttributeValue.php b/src/Domain/Identity/Model/AdminAttributeValue.php index 3d99ba73..35188ec6 100644 --- a/src/Domain/Identity/Model/AdminAttributeValue.php +++ b/src/Domain/Identity/Model/AdminAttributeValue.php @@ -9,7 +9,7 @@ use PhpList\Core\Domain\Identity\Repository\AdminAttributeValueRepository; #[ORM\Entity(repositoryClass: AdminAttributeValueRepository::class)] -#[ORM\Table(name: 'phplist_admin_attribute')] +#[ORM\Table(name: 'admin_attribute')] #[ORM\HasLifecycleCallbacks] class AdminAttributeValue implements DomainModel { diff --git a/src/Domain/Identity/Model/AdminLogin.php b/src/Domain/Identity/Model/AdminLogin.php index 91be3331..74d9abee 100644 --- a/src/Domain/Identity/Model/AdminLogin.php +++ b/src/Domain/Identity/Model/AdminLogin.php @@ -12,7 +12,7 @@ use PhpList\Core\Domain\Identity\Repository\AdminLoginRepository; #[ORM\Entity(repositoryClass: AdminLoginRepository::class)] -#[ORM\Table(name: 'phplist_admin_login')] +#[ORM\Table(name: 'admin_login')] #[ORM\HasLifecycleCallbacks] class AdminLogin implements DomainModel, Identity { diff --git a/src/Domain/Identity/Model/AdminPasswordRequest.php b/src/Domain/Identity/Model/AdminPasswordRequest.php index 0d761adf..00b46606 100644 --- a/src/Domain/Identity/Model/AdminPasswordRequest.php +++ b/src/Domain/Identity/Model/AdminPasswordRequest.php @@ -11,7 +11,7 @@ use PhpList\Core\Domain\Identity\Repository\AdminPasswordRequestRepository; #[ORM\Entity(repositoryClass: AdminPasswordRequestRepository::class)] -#[ORM\Table(name: 'phplist_admin_password_request')] +#[ORM\Table(name: 'admin_password_request')] class AdminPasswordRequest implements DomainModel, Identity { #[ORM\Id] @@ -24,7 +24,7 @@ class AdminPasswordRequest implements DomainModel, Identity #[ORM\ManyToOne(targetEntity: Administrator::class)] #[ORM\JoinColumn(name: 'admin', referencedColumnName: 'id', nullable: true)] - private Administrator $administrator; + private ?Administrator $administrator; #[ORM\Column(name: 'key_value', type: 'string', length: 32)] private string $keyValue; @@ -46,7 +46,7 @@ public function getDate(): DateTime return $this->date; } - public function getAdmin(): Administrator + public function getAdmin(): ?Administrator { return $this->administrator; } diff --git a/src/Domain/Identity/Model/Administrator.php b/src/Domain/Identity/Model/Administrator.php index 2f3de5eb..d640f245 100644 --- a/src/Domain/Identity/Model/Administrator.php +++ b/src/Domain/Identity/Model/Administrator.php @@ -25,7 +25,7 @@ * @author Tatevik Grigoryan */ #[ORM\Entity(repositoryClass: AdministratorRepository::class)] -#[ORM\Table(name: 'phplist_admin')] +#[ORM\Table(name: 'admin')] #[ORM\UniqueConstraint(name: 'phplist_admin_loginnameidx', columns: ['loginname'])] #[ORM\HasLifecycleCallbacks] class Administrator implements DomainModel, Identity, CreationDate, ModificationDate @@ -36,7 +36,7 @@ class Administrator implements DomainModel, Identity, CreationDate, Modification private ?int $id = null; #[ORM\Column(name: 'created', type: 'datetime', nullable: false)] - protected ?DateTime $createdAt = null; + protected DateTime $createdAt; #[ORM\Column(name: 'modified', type: 'datetime', nullable: false)] private DateTime $updatedAt; @@ -68,6 +68,9 @@ class Administrator implements DomainModel, Identity, CreationDate, Modification #[ORM\Column(name: 'privileges', type: 'text', nullable: true)] private ?string $privileges = null; + /** + * @var Collection + */ #[ORM\OneToMany(targetEntity: SubscriberList::class, mappedBy: 'owner')] private Collection $ownedLists; @@ -84,7 +87,7 @@ public function getId(): ?int return $this->id; } - public function getCreatedAt(): ?DateTime + public function getCreatedAt(): DateTime { return $this->createdAt; } diff --git a/src/Domain/Identity/Model/AdministratorToken.php b/src/Domain/Identity/Model/AdministratorToken.php index 4e37b2b5..eebef827 100644 --- a/src/Domain/Identity/Model/AdministratorToken.php +++ b/src/Domain/Identity/Model/AdministratorToken.php @@ -19,7 +19,7 @@ * @author Tateik Grigoryan */ #[ORM\Entity(repositoryClass: AdministratorTokenRepository::class)] -#[ORM\Table(name: 'phplist_admintoken')] +#[ORM\Table(name: 'admintoken')] #[ORM\HasLifecycleCallbacks] class AdministratorToken implements DomainModel, Identity, CreationDate { @@ -35,14 +35,14 @@ class AdministratorToken implements DomainModel, Identity, CreationDate #[ORM\Column(name: 'expires', type: 'datetime')] #[SerializedName('expiry_date')] - private ?DateTime $expiry = null; + private DateTime $expiry; #[ORM\Column(name: 'value')] #[SerializedName('key')] private string $key = ''; #[ORM\ManyToOne(targetEntity: Administrator::class)] - #[ORM\JoinColumn(name: 'adminid', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'adminid', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] private Administrator $administrator; public function __construct(Administrator $administrator) diff --git a/src/Domain/Identity/Repository/AdministratorRepository.php b/src/Domain/Identity/Repository/AdministratorRepository.php index 640a0a55..5973eb25 100644 --- a/src/Domain/Identity/Repository/AdministratorRepository.php +++ b/src/Domain/Identity/Repository/AdministratorRepository.php @@ -10,7 +10,7 @@ use PhpList\Core\Domain\Common\Repository\CursorPaginationTrait; use PhpList\Core\Domain\Common\Repository\Interfaces\PaginatableRepositoryInterface; use PhpList\Core\Domain\Identity\Model\Administrator; -use PhpList\Core\Security\HashGenerator; +use PhpList\Core\Domain\Identity\Service\HashGenerator; /** * Repository for Administrator models. @@ -45,15 +45,27 @@ public function __construct( */ public function findOneByLoginCredentials(string $loginName, string $plainTextPassword): ?Administrator { - $passwordHash = $this->hashGenerator->createPasswordHash($plainTextPassword); - - return $this->findOneBy( + /** @var Administrator|null $administrator */ + $administrator = $this->findOneBy( [ 'loginName' => $loginName, - 'passwordHash' => $passwordHash, 'superUser' => true, ] ); + + $passwordHash = $administrator?->getPasswordHash(); + if ($administrator === null || $passwordHash === null + || !$this->hashGenerator->verifyPassword($plainTextPassword, $passwordHash) + ) { + return null; + } + + if ($this->hashGenerator->isLegacyHash($passwordHash)) { + $administrator->setPasswordHash($this->hashGenerator->createPasswordHash($plainTextPassword)); + $this->save($administrator); + } + + return $administrator; } /** @return Administrator[] */ diff --git a/src/Security/Authentication.php b/src/Domain/Identity/Service/Authentication.php similarity index 93% rename from src/Security/Authentication.php rename to src/Domain/Identity/Service/Authentication.php index 5c6d69c4..152fb1c0 100644 --- a/src/Security/Authentication.php +++ b/src/Domain/Identity/Service/Authentication.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Security; +namespace PhpList\Core\Domain\Identity\Service; use Doctrine\ORM\EntityNotFoundException; use PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository; @@ -51,11 +51,7 @@ public function authenticateByApiKey(Request $request): ?Administrator return null; } - /** @var Administrator|null $administrator */ $administrator = $token->getAdministrator(); - if ($administrator === null) { - return null; - } try { // This checks for cases where a superuser created a session key and then got their super user diff --git a/src/Domain/Identity/Service/HashGenerator.php b/src/Domain/Identity/Service/HashGenerator.php new file mode 100644 index 00000000..f104da6a --- /dev/null +++ b/src/Domain/Identity/Service/HashGenerator.php @@ -0,0 +1,51 @@ + + */ +class HashGenerator +{ + /** + * Legacy algorithm that older password hashes in the database may still use. + * + * @var string + */ + const LEGACY_PASSWORD_HASH_ALGORITHM = 'sha256'; + + public function createPasswordHash(string $plainTextPassword): string + { + return password_hash($plainTextPassword, PASSWORD_DEFAULT); + } + + /** + * Checks a plaintext password against a stored hash. + * + * Hashes created by {@see createPasswordHash()} are verified with `password_verify()`. + * As a fallback, this also accepts hashes created by the old, unsalted + * sha256-based scheme, so administrators with pre-existing hashes can still log in. + */ + public function verifyPassword(string $plainTextPassword, string $hash): bool + { + if (password_verify($plainTextPassword, $hash)) { + return true; + } + + return $this->isLegacyHash($hash) + && hash_equals(hash(static::LEGACY_PASSWORD_HASH_ALGORITHM, $plainTextPassword), $hash); + } + + /** + * Checks whether $hash was created by the old, unsalted sha256-based scheme + * rather than by {@see createPasswordHash()}. + */ + public function isLegacyHash(string $hash): bool + { + return preg_match('/^[0-9a-f]{64}$/', $hash) === 1; + } +} diff --git a/src/Domain/Identity/Service/Manager/AdminAttributeDefinitionManager.php b/src/Domain/Identity/Service/Manager/AdminAttributeDefinitionManager.php index e66f9fe4..62106eaf 100644 --- a/src/Domain/Identity/Service/Manager/AdminAttributeDefinitionManager.php +++ b/src/Domain/Identity/Service/Manager/AdminAttributeDefinitionManager.php @@ -5,7 +5,7 @@ namespace PhpList\Core\Domain\Identity\Service\Manager; use PhpList\Core\Domain\Common\Model\PaginatedResult; -use PhpList\Core\Domain\Identity\Exception\AttributeDefinitionCreationException; +use PhpList\Core\Domain\Common\Exception\AttributeDefinitionCreationException; use PhpList\Core\Domain\Identity\Model\AdminAttributeDefinition; use PhpList\Core\Domain\Identity\Model\Dto\AdminAttributeDefinitionDto; use PhpList\Core\Domain\Identity\Repository\AdminAttributeDefinitionRepository; diff --git a/src/Domain/Identity/Service/Manager/AdministratorManager.php b/src/Domain/Identity/Service/Manager/AdministratorManager.php index 940eaa42..a318556d 100644 --- a/src/Domain/Identity/Service/Manager/AdministratorManager.php +++ b/src/Domain/Identity/Service/Manager/AdministratorManager.php @@ -8,7 +8,7 @@ use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Identity\Model\Dto\CreateAdministratorDto; use PhpList\Core\Domain\Identity\Model\Dto\UpdateAdministratorDto; -use PhpList\Core\Security\HashGenerator; +use PhpList\Core\Domain\Identity\Service\HashGenerator; class AdministratorManager { diff --git a/src/Domain/Identity/Service/Manager/PasswordManager.php b/src/Domain/Identity/Service/Manager/PasswordManager.php index 01f9bb7d..28b5bbbe 100644 --- a/src/Domain/Identity/Service/Manager/PasswordManager.php +++ b/src/Domain/Identity/Service/Manager/PasswordManager.php @@ -10,7 +10,7 @@ use PhpList\Core\Domain\Identity\Repository\AdministratorRepository; use PhpList\Core\Domain\Identity\Repository\AdminPasswordRequestRepository; use PhpList\Core\Domain\Messaging\Message\PasswordResetMessage; -use PhpList\Core\Security\HashGenerator; +use PhpList\Core\Domain\Identity\Service\HashGenerator; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Contracts\Translation\TranslatorInterface; diff --git a/src/Domain/Identity/Validator/AttributeTypeValidator.php b/src/Domain/Identity/Validator/AttributeTypeValidator.php index c7bb3853..177657fa 100644 --- a/src/Domain/Identity/Validator/AttributeTypeValidator.php +++ b/src/Domain/Identity/Validator/AttributeTypeValidator.php @@ -4,71 +4,18 @@ namespace PhpList\Core\Domain\Identity\Validator; -use InvalidArgumentException; use PhpList\Core\Domain\Common\Model\AttributeTypeEnum; -use PhpList\Core\Domain\Common\Model\ValidationContext; -use PhpList\Core\Domain\Common\Validator\ValidatorInterface; -use Symfony\Component\Validator\Exception\ValidatorException; -use Symfony\Contracts\Translation\TranslatorInterface; -use Throwable; +use PhpList\Core\Domain\Common\Validator\AbstractAttributeTypeValidator; -class AttributeTypeValidator implements ValidatorInterface +class AttributeTypeValidator extends AbstractAttributeTypeValidator { - public function __construct(private readonly TranslatorInterface $translator) - { - } - private const VALID_TYPES = [ AttributeTypeEnum::TextLine, AttributeTypeEnum::Hidden, ]; - public function validate(mixed $value, ValidationContext $context = null): void + protected function getValidTypes(): array { - $enum = $this->normalizeToEnum($value); - - if (!in_array($enum, self::VALID_TYPES, true)) { - $validList = implode(', ', array_map( - static fn(AttributeTypeEnum $enum) => $enum->value, - self::VALID_TYPES - )); - - $message = $this->translator->trans( - 'Invalid attribute type: "%type%". Valid types are: %valid_types%', - [ - '%type%' => $enum->value, - '%valid_types%' => $validList, - ] - ); - - throw new ValidatorException($message); - } - } - - /** - * @throws InvalidArgumentException if value cannot be converted to AttributeTypeEnum - */ - private function normalizeToEnum(mixed $value): AttributeTypeEnum - { - if ($value instanceof AttributeTypeEnum) { - return $value; - } - - if (is_string($value)) { - try { - return AttributeTypeEnum::from($value); - } catch (Throwable) { - $lower = strtolower($value); - foreach (AttributeTypeEnum::cases() as $case) { - if ($case->value === $lower) { - return $case; - } - } - } - } - - throw new InvalidArgumentException( - $this->translator->trans('Value must be an AttributeTypeEnum or string.') - ); + return self::VALID_TYPES; } } diff --git a/src/Bounce/Command/ProcessBouncesCommand.php b/src/Domain/Messaging/Command/ProcessBouncesCommand.php similarity index 91% rename from src/Bounce/Command/ProcessBouncesCommand.php rename to src/Domain/Messaging/Command/ProcessBouncesCommand.php index fcb37ba2..52e22469 100644 --- a/src/Bounce/Command/ProcessBouncesCommand.php +++ b/src/Domain/Messaging/Command/ProcessBouncesCommand.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Command; +namespace PhpList\Core\Domain\Messaging\Command; use Doctrine\ORM\EntityManagerInterface; use Exception; -use PhpList\Core\Bounce\Service\ConsecutiveBounceHandler; -use PhpList\Core\Bounce\Service\LockService; -use PhpList\Core\Bounce\Service\Processor\AdvancedBounceRulesProcessor; -use PhpList\Core\Bounce\Service\Processor\BounceProtocolProcessor; -use PhpList\Core\Bounce\Service\Processor\UnidentifiedBounceReprocessor; +use PhpList\Core\Domain\Messaging\Service\ConsecutiveBounceHandler; +use PhpList\Core\Domain\Messaging\Service\LockService; +use PhpList\Core\Domain\Messaging\Service\Processor\AdvancedBounceRulesProcessor; +use PhpList\Core\Domain\Messaging\Service\Processor\BounceProtocolProcessor; +use PhpList\Core\Domain\Messaging\Service\Processor\UnidentifiedBounceReprocessor; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; diff --git a/src/Domain/Messaging/Command/ProcessQueueCommand.php b/src/Domain/Messaging/Command/ProcessQueueCommand.php index 080c24cb..69bf967b 100644 --- a/src/Domain/Messaging/Command/ProcessQueueCommand.php +++ b/src/Domain/Messaging/Command/ProcessQueueCommand.php @@ -27,31 +27,16 @@ )] class ProcessQueueCommand extends Command { - private MessageRepository $messageRepository; - private LockFactory $lockFactory; - private MessageProcessingPreparator $messagePreparator; - private MessageBusInterface $messageBus; - private ConfigProvider $configProvider; - private TranslatorInterface $translator; - private EntityManagerInterface $entityManager; - public function __construct( - MessageRepository $messageRepository, - LockFactory $lockFactory, - MessageProcessingPreparator $messagePreparator, - MessageBusInterface $messageBus, - ConfigProvider $configProvider, - TranslatorInterface $translator, - EntityManagerInterface $entityManager, + private readonly MessageRepository $messageRepository, + private readonly LockFactory $lockFactory, + private readonly MessageProcessingPreparator $messagePreparator, + private readonly MessageBusInterface $messageBus, + private readonly ConfigProvider $configProvider, + private readonly TranslatorInterface $translator, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct(); - $this->messageRepository = $messageRepository; - $this->lockFactory = $lockFactory; - $this->messagePreparator = $messagePreparator; - $this->messageBus = $messageBus; - $this->configProvider = $configProvider; - $this->translator = $translator; - $this->entityManager = $entityManager; } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Domain/Messaging/Command/SendTestEmailCommand.php b/src/Domain/Messaging/Command/SendTestEmailCommand.php index e9670239..2766af9d 100644 --- a/src/Domain/Messaging/Command/SendTestEmailCommand.php +++ b/src/Domain/Messaging/Command/SendTestEmailCommand.php @@ -21,14 +21,11 @@ )] class SendTestEmailCommand extends Command { - private EmailService $emailService; - private TranslatorInterface $translator; - - public function __construct(EmailService $emailService, TranslatorInterface $translator) - { + public function __construct( + private readonly EmailService $emailService, + private readonly TranslatorInterface $translator + ) { parent::__construct(); - $this->emailService = $emailService; - $this->translator = $translator; } protected function configure(): void diff --git a/src/Bounce/Exception/ImapConnectionException.php b/src/Domain/Messaging/Exception/ImapConnectionException.php similarity index 84% rename from src/Bounce/Exception/ImapConnectionException.php rename to src/Domain/Messaging/Exception/ImapConnectionException.php index 58d3495d..8e5295e2 100644 --- a/src/Bounce/Exception/ImapConnectionException.php +++ b/src/Domain/Messaging/Exception/ImapConnectionException.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Exception; +namespace PhpList\Core\Domain\Messaging\Exception; use RuntimeException; use Throwable; diff --git a/src/Bounce/Exception/OpenMboxFileException.php b/src/Domain/Messaging/Exception/OpenMboxFileException.php similarity index 84% rename from src/Bounce/Exception/OpenMboxFileException.php rename to src/Domain/Messaging/Exception/OpenMboxFileException.php index c5dc775f..2fc7c458 100644 --- a/src/Bounce/Exception/OpenMboxFileException.php +++ b/src/Domain/Messaging/Exception/OpenMboxFileException.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Exception; +namespace PhpList\Core\Domain\Messaging\Exception; use RuntimeException; use Throwable; diff --git a/src/Domain/Messaging/MessageHandler/CampaignProcessor/CampaignProcessorMessageHandler.php b/src/Domain/Messaging/MessageHandler/CampaignProcessor/CampaignProcessorMessageHandler.php index e1aa9219..ad8d0f48 100644 --- a/src/Domain/Messaging/MessageHandler/CampaignProcessor/CampaignProcessorMessageHandler.php +++ b/src/Domain/Messaging/MessageHandler/CampaignProcessor/CampaignProcessorMessageHandler.php @@ -46,8 +46,8 @@ use Throwable; /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings("PHPMD.CouplingBetweenObjects") + * @SuppressWarnings("PHPMD.ExcessiveParameterList") */ #[AsMessageHandler] class CampaignProcessorMessageHandler diff --git a/src/Domain/Messaging/MessageHandler/CampaignProcessor/TestCampaignProcessorMessageHandler.php b/src/Domain/Messaging/MessageHandler/CampaignProcessor/TestCampaignProcessorMessageHandler.php index 8ae47ea7..95b0a107 100644 --- a/src/Domain/Messaging/MessageHandler/CampaignProcessor/TestCampaignProcessorMessageHandler.php +++ b/src/Domain/Messaging/MessageHandler/CampaignProcessor/TestCampaignProcessorMessageHandler.php @@ -32,8 +32,8 @@ use Throwable; /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings("PHPMD.CouplingBetweenObjects") + * @SuppressWarnings("PHPMD.ExcessiveParameterList") */ #[AsMessageHandler] class TestCampaignProcessorMessageHandler diff --git a/src/Domain/Messaging/Model/Attachment.php b/src/Domain/Messaging/Model/Attachment.php index d49cd386..a8b38b4b 100644 --- a/src/Domain/Messaging/Model/Attachment.php +++ b/src/Domain/Messaging/Model/Attachment.php @@ -10,7 +10,7 @@ use PhpList\Core\Domain\Messaging\Repository\AttachmentRepository; #[ORM\Entity(repositoryClass: AttachmentRepository::class)] -#[ORM\Table(name: 'phplist_attachment')] +#[ORM\Table(name: 'attachment')] class Attachment implements DomainModel, Identity { public const FORWARD = 'forwarded'; diff --git a/src/Domain/Messaging/Model/Bounce.php b/src/Domain/Messaging/Model/Bounce.php index 54e5895d..071b869f 100644 --- a/src/Domain/Messaging/Model/Bounce.php +++ b/src/Domain/Messaging/Model/Bounce.php @@ -11,7 +11,7 @@ use PhpList\Core\Domain\Messaging\Repository\BounceRepository; #[ORM\Entity(repositoryClass: BounceRepository::class)] -#[ORM\Table(name: 'phplist_bounce')] +#[ORM\Table(name: 'bounce')] #[ORM\Index(name: 'phplist_bounce_dateindex', columns: ['date'])] #[ORM\Index(name: 'phplist_bounce_statusidx', columns: ['status'])] class Bounce implements DomainModel, Identity diff --git a/src/Domain/Messaging/Model/BounceRegex.php b/src/Domain/Messaging/Model/BounceRegex.php index c54ca7c0..5d0d0521 100644 --- a/src/Domain/Messaging/Model/BounceRegex.php +++ b/src/Domain/Messaging/Model/BounceRegex.php @@ -10,7 +10,7 @@ use PhpList\Core\Domain\Messaging\Repository\BounceRegexRepository; #[ORM\Entity(repositoryClass: BounceRegexRepository::class)] -#[ORM\Table(name: 'phplist_bounceregex')] +#[ORM\Table(name: 'bounceregex')] #[ORM\UniqueConstraint(name: 'phplist_bounceregex_regex', columns: ['regexhash'])] class BounceRegex implements DomainModel, Identity { diff --git a/src/Domain/Messaging/Model/BounceRegexBounce.php b/src/Domain/Messaging/Model/BounceRegexBounce.php index e815cd1f..c50d20d5 100644 --- a/src/Domain/Messaging/Model/BounceRegexBounce.php +++ b/src/Domain/Messaging/Model/BounceRegexBounce.php @@ -9,7 +9,7 @@ use PhpList\Core\Domain\Messaging\Repository\BounceRegexBounceRepository; #[ORM\Entity(repositoryClass: BounceRegexBounceRepository::class)] -#[ORM\Table(name: 'phplist_bounceregex_bounce')] +#[ORM\Table(name: 'bounceregex_bounce')] class BounceRegexBounce implements DomainModel { #[ORM\Id] diff --git a/src/Domain/Messaging/Model/Dto/MessagePrecacheDto.php b/src/Domain/Messaging/Model/Dto/MessagePrecacheDto.php index 3b0f0d24..57cbc32b 100644 --- a/src/Domain/Messaging/Model/Dto/MessagePrecacheDto.php +++ b/src/Domain/Messaging/Model/Dto/MessagePrecacheDto.php @@ -4,7 +4,7 @@ namespace PhpList\Core\Domain\Messaging\Model\Dto; -/** @SuppressWarnings(TooManyFields) */ +/** @SuppressWarnings("TooManyFields") */ class MessagePrecacheDto { public string $replyToEmail = ''; diff --git a/src/Domain/Messaging/Model/Filter/MessageFilter.php b/src/Domain/Messaging/Model/Filter/MessageFilter.php index ccb5b1ac..470c1890 100644 --- a/src/Domain/Messaging/Model/Filter/MessageFilter.php +++ b/src/Domain/Messaging/Model/Filter/MessageFilter.php @@ -12,6 +12,8 @@ class MessageFilter extends PaginatedFilter implements FilterRequestInterface { private ?Administrator $owner = null; private ?string $subject = null; + private ?string $status = null; + private string $sortOrder = 'asc'; public function getOwner(): ?Administrator { @@ -37,4 +39,31 @@ public function setSubject(?string $subject): self $this->subject = $subject; return $this; } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(?string $status): self + { + if ($status !== null) { + $status = trim($status); + } + $this->status = $status; + return $this; + } + + public function getSortOrder(): string + { + return $this->sortOrder; + } + + public function setSortOrder(string $sortOrder): self + { + if (in_array($sortOrder, ['asc', 'desc'], true)) { + $this->sortOrder = $sortOrder; + } + return $this; + } } diff --git a/src/Domain/Messaging/Model/ListMessage.php b/src/Domain/Messaging/Model/ListMessage.php index 3a5d655a..e20b3d9d 100644 --- a/src/Domain/Messaging/Model/ListMessage.php +++ b/src/Domain/Messaging/Model/ListMessage.php @@ -14,7 +14,7 @@ use PhpList\Core\Domain\Subscription\Model\SubscriberList; #[ORM\Entity(repositoryClass: ListMessageRepository::class)] -#[ORM\Table(name: 'phplist_listmessage')] +#[ORM\Table(name: 'listmessage')] #[ORM\UniqueConstraint(name: 'phplist_listmessage_messageid', columns: ['messageid', 'listid'])] #[ORM\Index(name: 'phplist_listmessage_listmessageidx', columns: ['listid', 'messageid'])] #[ORM\HasLifecycleCallbacks] @@ -37,7 +37,7 @@ class ListMessage implements DomainModel, Identity, ModificationDate private ?DateTimeInterface $entered = null; #[ORM\Column(name: 'modified', type: 'datetime')] - private ?DateTime $updatedAt = null; + private DateTime $updatedAt; public function __construct(Message $message, SubscriberList $subscriberList) { @@ -67,7 +67,7 @@ public function getEntered(): ?DateTimeInterface return $this->entered; } - public function getUpdatedAt(): ?DateTime + public function getUpdatedAt(): DateTime { return $this->updatedAt; } diff --git a/src/Domain/Messaging/Model/Message.php b/src/Domain/Messaging/Model/Message.php index 4d5f4e8f..94faee06 100644 --- a/src/Domain/Messaging/Model/Message.php +++ b/src/Domain/Messaging/Model/Message.php @@ -22,8 +22,9 @@ use PhpList\Core\Domain\Messaging\Repository\MessageRepository; #[ORM\Entity(repositoryClass: MessageRepository::class)] -#[ORM\Table(name: 'phplist_message')] +#[ORM\Table(name: 'message')] #[ORM\Index(name: 'phplist_message_uuididx', columns: ['uuid'])] +#[ORM\Index(name: 'phplist_message_sentidx', columns: ['sent'])] #[ORM\HasLifecycleCallbacks] class Message implements DomainModel, Identity, ModificationDate, OwnableInterface { diff --git a/src/Domain/Messaging/Model/Message/MessageStatus.php b/src/Domain/Messaging/Model/Message/MessageStatus.php index 789f07c2..7f6e0daa 100644 --- a/src/Domain/Messaging/Model/Message/MessageStatus.php +++ b/src/Domain/Messaging/Model/Message/MessageStatus.php @@ -12,7 +12,6 @@ enum MessageStatus: string case InProcess = 'inprocess'; case Sent = 'sent'; case Suspended = 'suspended'; - case Requeued = 'requeued'; /** * Allowed transitions for each state @@ -23,12 +22,10 @@ public function allowedTransitions(): array { return match ($this) { self::Draft => [self::Prepared, self::Submitted], - self::Suspended => [self::Submitted, self::Requeued], + self::Suspended, self::Sent => [self::Submitted], self::Submitted => [self::Prepared, self::InProcess, self::Suspended], self::Prepared => [self::InProcess, self::Suspended], self::InProcess => [self::Sent, self::Suspended, self::Submitted], - self::Requeued => [self::InProcess, self::Suspended], - self::Sent => [self::Requeued], }; } diff --git a/src/Domain/Messaging/Model/MessageAttachment.php b/src/Domain/Messaging/Model/MessageAttachment.php index e26d0d87..2007ad5c 100644 --- a/src/Domain/Messaging/Model/MessageAttachment.php +++ b/src/Domain/Messaging/Model/MessageAttachment.php @@ -9,7 +9,7 @@ use PhpList\Core\Domain\Messaging\Repository\MessageAttachmentRepository; #[ORM\Entity(repositoryClass: MessageAttachmentRepository::class)] -#[ORM\Table(name: 'phplist_message_attachment')] +#[ORM\Table(name: 'message_attachment')] #[ORM\Index(name: 'phplist_message_attachment_messageattidx', columns: ['messageid', 'attachmentid'])] #[ORM\Index(name: 'phplist_message_attachment_messageidx', columns: ['messageid'])] class MessageAttachment implements Identity diff --git a/src/Domain/Messaging/Model/MessageData.php b/src/Domain/Messaging/Model/MessageData.php index 56744251..d364889c 100644 --- a/src/Domain/Messaging/Model/MessageData.php +++ b/src/Domain/Messaging/Model/MessageData.php @@ -9,7 +9,7 @@ use PhpList\Core\Domain\Messaging\Repository\MessageDataRepository; #[ORM\Entity(repositoryClass: MessageDataRepository::class)] -#[ORM\Table(name: 'phplist_messagedata')] +#[ORM\Table(name: 'messagedata')] class MessageData implements DomainModel { #[ORM\Id] diff --git a/src/Domain/Messaging/Model/SendProcess.php b/src/Domain/Messaging/Model/SendProcess.php index 5faeaf35..2c68ff9f 100644 --- a/src/Domain/Messaging/Model/SendProcess.php +++ b/src/Domain/Messaging/Model/SendProcess.php @@ -12,7 +12,7 @@ use PhpList\Core\Domain\Messaging\Repository\SendProcessRepository; #[ORM\Entity(repositoryClass: SendProcessRepository::class)] -#[ORM\Table(name: 'phplist_sendprocess')] +#[ORM\Table(name: 'sendprocess')] #[ORM\HasLifecycleCallbacks] class SendProcess implements DomainModel, Identity, ModificationDate { @@ -22,7 +22,7 @@ class SendProcess implements DomainModel, Identity, ModificationDate private ?int $id = null; #[ORM\Column(name: 'modified', type: 'datetime')] - private ?DateTime $updatedAt = null; + private DateTime $updatedAt; #[ORM\Column(name: 'started', type: 'datetime', nullable: true)] private ?DateTime $started = null; @@ -36,12 +36,17 @@ class SendProcess implements DomainModel, Identity, ModificationDate #[ORM\Column(name: 'page', type: 'string', length: 100, nullable: true)] private ?string $page = null; + public function __construct() + { + $this->updatedAt = new DateTime(); + } + public function getId(): ?int { return $this->id; } - public function getUpdatedAt(): ?DateTime + public function getUpdatedAt(): DateTime { return $this->updatedAt; } diff --git a/src/Domain/Messaging/Model/Template.php b/src/Domain/Messaging/Model/Template.php index dc1b67a0..b54bc125 100644 --- a/src/Domain/Messaging/Model/Template.php +++ b/src/Domain/Messaging/Model/Template.php @@ -12,7 +12,7 @@ use PhpList\Core\Domain\Messaging\Repository\TemplateRepository; #[ORM\Entity(repositoryClass: TemplateRepository::class)] -#[ORM\Table(name: 'phplist_template')] +#[ORM\Table(name: 'template')] #[ORM\UniqueConstraint(name: 'phplist_template_title', columns: ['title'])] class Template implements DomainModel, Identity { @@ -33,6 +33,9 @@ class Template implements DomainModel, Identity #[ORM\Column(name: 'listorder', type: 'integer', nullable: true)] private ?int $listOrder = null; + /** + * @var Collection + */ #[ORM\OneToMany( targetEntity: TemplateImage::class, mappedBy: 'template', diff --git a/src/Domain/Messaging/Model/TemplateImage.php b/src/Domain/Messaging/Model/TemplateImage.php index c1c5c8c4..a0da4692 100644 --- a/src/Domain/Messaging/Model/TemplateImage.php +++ b/src/Domain/Messaging/Model/TemplateImage.php @@ -10,7 +10,7 @@ use PhpList\Core\Domain\Messaging\Repository\TemplateImageRepository; #[ORM\Entity(repositoryClass: TemplateImageRepository::class)] -#[ORM\Table(name: 'phplist_templateimage')] +#[ORM\Table(name: 'templateimage')] #[ORM\Index(name: 'phplist_templateimage_templateidx', columns: ['template'])] class TemplateImage implements DomainModel, Identity { diff --git a/src/Domain/Messaging/Model/UserMessage.php b/src/Domain/Messaging/Model/UserMessage.php index d5fe202c..93b457f3 100644 --- a/src/Domain/Messaging/Model/UserMessage.php +++ b/src/Domain/Messaging/Model/UserMessage.php @@ -12,7 +12,7 @@ use PhpList\Core\Domain\Subscription\Model\Subscriber; #[ORM\Entity(repositoryClass: UserMessageRepository::class)] -#[ORM\Table(name: 'phplist_usermessage')] +#[ORM\Table(name: 'usermessage')] #[ORM\Index(name: 'phplist_usermessage_enteredindex', columns: ['entered'])] #[ORM\Index(name: 'phplist_usermessage_messageidindex', columns: ['messageid'])] #[ORM\Index(name: 'phplist_usermessage_statusidx', columns: ['status'])] diff --git a/src/Domain/Messaging/Model/UserMessageBounce.php b/src/Domain/Messaging/Model/UserMessageBounce.php index 3b58bf47..2a7ef519 100644 --- a/src/Domain/Messaging/Model/UserMessageBounce.php +++ b/src/Domain/Messaging/Model/UserMessageBounce.php @@ -11,11 +11,12 @@ use PhpList\Core\Domain\Messaging\Repository\UserMessageBounceRepository; #[ORM\Entity(repositoryClass: UserMessageBounceRepository::class)] -#[ORM\Table(name: 'phplist_user_message_bounce')] +#[ORM\Table(name: 'user_message_bounce')] #[ORM\Index(name: 'phplist_user_message_bounce_bounceidx', columns: ['bounce'])] #[ORM\Index(name: 'phplist_user_message_bounce_msgidx', columns: ['message'])] #[ORM\Index(name: 'phplist_user_message_bounce_umbindex', columns: ['user', 'message', 'bounce'])] #[ORM\Index(name: 'phplist_user_message_bounce_useridx', columns: ['user'])] +// todo: #[ORM\Index(name: 'phplist_user_message_bounce_timeidx', columns: ['time'])] class UserMessageBounce implements DomainModel, Identity { #[ORM\Id] diff --git a/src/Domain/Messaging/Model/UserMessageForward.php b/src/Domain/Messaging/Model/UserMessageForward.php index 3b920189..1dd32806 100644 --- a/src/Domain/Messaging/Model/UserMessageForward.php +++ b/src/Domain/Messaging/Model/UserMessageForward.php @@ -11,7 +11,7 @@ use PhpList\Core\Domain\Messaging\Repository\UserMessageForwardRepository; #[ORM\Entity(repositoryClass: UserMessageForwardRepository::class)] -#[ORM\Table(name: 'phplist_user_message_forward')] +#[ORM\Table(name: 'user_message_forward')] #[ORM\Index(name: 'phplist_user_message_forward_messageidx', columns: ['message'])] #[ORM\Index(name: 'phplist_user_message_forward_useridx', columns: ['user'])] #[ORM\Index(name: 'phplist_user_message_forward_usermessageidx', columns: ['user', 'message'])] diff --git a/src/Domain/Messaging/Repository/MessageRepository.php b/src/Domain/Messaging/Repository/MessageRepository.php index d18ce68b..13394794 100644 --- a/src/Domain/Messaging/Repository/MessageRepository.php +++ b/src/Domain/Messaging/Repository/MessageRepository.php @@ -48,7 +48,12 @@ public function findById(int $id): ?Message ->getOneOrNullResult(); } - /** @return PaginatedResult */ + /** + * @return PaginatedResult + * @SuppressWarnings("CyclomaticComplexity") + * @SuppressWarnings("NPathComplexity") + * + */ public function getFilteredAfterId(FilterRequestInterface $filter): PaginatedResult { $lastId = $filter->getLastId(); @@ -56,7 +61,9 @@ public function getFilteredAfterId(FilterRequestInterface $filter): PaginatedRes $queryBuilder = $this->createQueryBuilder('m'); if ($filter instanceof MessageFilter && $filter->getOwner() !== null) { - $queryBuilder->andWhere('IDENTITY(m.owner) = :ownerId') + // Legacy/imported messages have no owner recorded - treat them as shared rather + // than invisible, instead of excluding them outright via a strict owner match. + $queryBuilder->andWhere('(m.owner IS NULL OR IDENTITY(m.owner) = :ownerId)') ->setParameter('ownerId', $filter->getOwner()->getId()); } @@ -65,17 +72,34 @@ public function getFilteredAfterId(FilterRequestInterface $filter): PaginatedRes ->setParameter('subject', '%' . $filter->getSubject() . '%'); } + if ($filter instanceof MessageFilter && $filter->getStatus() !== null) { + $statuses = array_values(array_filter(array_map('trim', explode(',', $filter->getStatus())))); + if (count($statuses) === 1) { + $queryBuilder->andWhere('m.metadata.status = :status') + ->setParameter('status', $statuses[0]); + } elseif (count($statuses) > 1) { + $queryBuilder->andWhere('m.metadata.status IN (:statuses)') + ->setParameter('statuses', $statuses); + } + } + $countQb = clone $queryBuilder; $total = (int) $countQb ->select('COUNT(DISTINCT m.id)') ->getQuery() ->getSingleScalarResult(); + $sortOrder = $filter instanceof MessageFilter ? $filter->getSortOrder() : 'asc'; + $comparison = $sortOrder === 'desc' ? '<' : '>'; + + if ($lastId > 0) { + $queryBuilder->andWhere(sprintf('m.id %s :lastId', $comparison)) + ->setParameter('lastId', $lastId); + } + /** @var list $items */ $items = $queryBuilder - ->andWhere('m.id > :lastId') - ->setParameter('lastId', $lastId) - ->orderBy('m.id', 'ASC') + ->orderBy('m.id', strtoupper($sortOrder)) ->setMaxResults($limit) ->getQuery() ->getResult(); @@ -116,7 +140,7 @@ public function getByStatusAndEmbargo(Message\MessageStatus $status, DateTimeImm { return $this->createQueryBuilder('m') ->where('m.metadata.status = :status') - ->andWhere('m.schedule.embargo IS NULL OR m.embargo <= :embargo') + ->andWhere('m.schedule.embargo IS NULL OR m.schedule.embargo <= :embargo') ->setParameter('status', $status->value) ->setParameter('embargo', $embargo) ->getQuery() diff --git a/src/Bounce/Service/BounceActionResolver.php b/src/Domain/Messaging/Service/BounceActionResolver.php similarity index 92% rename from src/Bounce/Service/BounceActionResolver.php rename to src/Domain/Messaging/Service/BounceActionResolver.php index 0359866c..93d432dd 100644 --- a/src/Bounce/Service/BounceActionResolver.php +++ b/src/Domain/Messaging/Service/BounceActionResolver.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service; +namespace PhpList\Core\Domain\Messaging\Service; -use PhpList\Core\Bounce\Service\Handler\BounceActionHandlerInterface; +use PhpList\Core\Domain\Messaging\Service\Handler\BounceActionHandlerInterface; use RuntimeException; class BounceActionResolver diff --git a/src/Bounce/Service/BounceProcessingServiceInterface.php b/src/Domain/Messaging/Service/BounceProcessingServiceInterface.php similarity index 77% rename from src/Bounce/Service/BounceProcessingServiceInterface.php rename to src/Domain/Messaging/Service/BounceProcessingServiceInterface.php index 8050a400..9d16702f 100644 --- a/src/Bounce/Service/BounceProcessingServiceInterface.php +++ b/src/Domain/Messaging/Service/BounceProcessingServiceInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service; +namespace PhpList\Core\Domain\Messaging\Service; interface BounceProcessingServiceInterface { diff --git a/src/Domain/Messaging/Service/Builder/MessageOptionsBuilder.php b/src/Domain/Messaging/Service/Builder/MessageOptionsBuilder.php index 91689d1e..9a6ea366 100644 --- a/src/Domain/Messaging/Service/Builder/MessageOptionsBuilder.php +++ b/src/Domain/Messaging/Service/Builder/MessageOptionsBuilder.php @@ -17,7 +17,7 @@ public function build(object $dto): MessageOptions } return new MessageOptions( - fromField: $dto->fromField ?? '', + fromField: $dto->fromField, toField: $dto->toField ?? '', replyTo: $dto->replyTo ?? '', userSelection: $dto->userSelection, diff --git a/src/Bounce/Service/ConsecutiveBounceHandler.php b/src/Domain/Messaging/Service/ConsecutiveBounceHandler.php similarity index 97% rename from src/Bounce/Service/ConsecutiveBounceHandler.php rename to src/Domain/Messaging/Service/ConsecutiveBounceHandler.php index 6a2687f2..3f1e34d1 100644 --- a/src/Bounce/Service/ConsecutiveBounceHandler.php +++ b/src/Domain/Messaging/Service/ConsecutiveBounceHandler.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service; +namespace PhpList\Core\Domain\Messaging\Service; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Messaging\Model\UserMessage; use PhpList\Core\Domain\Messaging\Model\UserMessageBounce; diff --git a/src/Domain/Messaging/Service/ForwardContentService.php b/src/Domain/Messaging/Service/ForwardContentService.php index cb1e505b..a260bead 100644 --- a/src/Domain/Messaging/Service/ForwardContentService.php +++ b/src/Domain/Messaging/Service/ForwardContentService.php @@ -5,6 +5,8 @@ namespace PhpList\Core\Domain\Messaging\Service; use PhpList\Core\Domain\Configuration\Model\OutputFormat; +use PhpList\Core\Domain\Messaging\Exception\EmailBlacklistedException; +use PhpList\Core\Domain\Messaging\Exception\InvalidRecipientOrSubjectException; use PhpList\Core\Domain\Messaging\Exception\MessageCacheMissingException; use PhpList\Core\Domain\Messaging\Model\Dto\MessageForwardDto; use PhpList\Core\Domain\Messaging\Model\Message; @@ -23,7 +25,7 @@ public function __construct( } /** @return array{Email, OutputFormat} - * @throws MessageCacheMissingException + * @throws MessageCacheMissingException | InvalidRecipientOrSubjectException | EmailBlacklistedException */ public function getContents( Message $campaign, diff --git a/src/Bounce/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php b/src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php similarity index 91% rename from src/Bounce/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php rename to src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php index c4171031..ddd56a47 100644 --- a/src/Bounce/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php +++ b/src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Handler; +namespace PhpList\Core\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; -use PhpList\Core\Bounce\Service\SubscriberBlacklistService; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\SubscriberBlacklistService; use PhpList\Core\Domain\Messaging\Model\BounceAction; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; use Symfony\Contracts\Translation\TranslatorInterface; diff --git a/src/Bounce/Service/Handler/BlacklistEmailHandler.php b/src/Domain/Messaging/Service/Handler/BlacklistEmailHandler.php similarity index 93% rename from src/Bounce/Service/Handler/BlacklistEmailHandler.php rename to src/Domain/Messaging/Service/Handler/BlacklistEmailHandler.php index 95c4f9e1..7a73add7 100644 --- a/src/Bounce/Service/Handler/BlacklistEmailHandler.php +++ b/src/Domain/Messaging/Service/Handler/BlacklistEmailHandler.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Handler; +namespace PhpList\Core\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\SubscriberBlacklistService; +use PhpList\Core\Domain\Messaging\Service\SubscriberBlacklistService; use PhpList\Core\Domain\Messaging\Model\BounceAction; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; use Symfony\Contracts\Translation\TranslatorInterface; diff --git a/src/Bounce/Service/Handler/BlacklistUserAndDeleteBounceHandler.php b/src/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandler.php similarity index 91% rename from src/Bounce/Service/Handler/BlacklistUserAndDeleteBounceHandler.php rename to src/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandler.php index 35d2202a..e90f8ddf 100644 --- a/src/Bounce/Service/Handler/BlacklistUserAndDeleteBounceHandler.php +++ b/src/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandler.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Handler; +namespace PhpList\Core\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; -use PhpList\Core\Bounce\Service\SubscriberBlacklistService; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\SubscriberBlacklistService; use PhpList\Core\Domain\Messaging\Model\BounceAction; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; use Symfony\Contracts\Translation\TranslatorInterface; diff --git a/src/Bounce/Service/Handler/BlacklistUserHandler.php b/src/Domain/Messaging/Service/Handler/BlacklistUserHandler.php similarity index 93% rename from src/Bounce/Service/Handler/BlacklistUserHandler.php rename to src/Domain/Messaging/Service/Handler/BlacklistUserHandler.php index a3dd9ef4..b69b467b 100644 --- a/src/Bounce/Service/Handler/BlacklistUserHandler.php +++ b/src/Domain/Messaging/Service/Handler/BlacklistUserHandler.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Handler; +namespace PhpList\Core\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\SubscriberBlacklistService; +use PhpList\Core\Domain\Messaging\Service\SubscriberBlacklistService; use PhpList\Core\Domain\Messaging\Model\BounceAction; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; use Symfony\Contracts\Translation\TranslatorInterface; diff --git a/src/Bounce/Service/Handler/BounceActionHandlerInterface.php b/src/Domain/Messaging/Service/Handler/BounceActionHandlerInterface.php similarity index 76% rename from src/Bounce/Service/Handler/BounceActionHandlerInterface.php rename to src/Domain/Messaging/Service/Handler/BounceActionHandlerInterface.php index ce43f7c7..6b90cb49 100644 --- a/src/Bounce/Service/Handler/BounceActionHandlerInterface.php +++ b/src/Domain/Messaging/Service/Handler/BounceActionHandlerInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Handler; +namespace PhpList\Core\Domain\Messaging\Service\Handler; interface BounceActionHandlerInterface { diff --git a/src/Bounce/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandler.php b/src/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandler.php similarity index 94% rename from src/Bounce/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandler.php rename to src/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandler.php index 32d129d2..2eabd11c 100644 --- a/src/Bounce/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandler.php +++ b/src/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandler.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Handler; +namespace PhpList\Core\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Messaging\Model\BounceAction; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; diff --git a/src/Bounce/Service/Handler/DeleteBounceHandler.php b/src/Domain/Messaging/Service/Handler/DeleteBounceHandler.php similarity index 82% rename from src/Bounce/Service/Handler/DeleteBounceHandler.php rename to src/Domain/Messaging/Service/Handler/DeleteBounceHandler.php index b3455614..a7643de7 100644 --- a/src/Bounce/Service/Handler/DeleteBounceHandler.php +++ b/src/Domain/Messaging/Service/Handler/DeleteBounceHandler.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Handler; +namespace PhpList\Core\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Messaging\Model\BounceAction; class DeleteBounceHandler implements BounceActionHandlerInterface diff --git a/src/Bounce/Service/Handler/DeleteUserAndBounceHandler.php b/src/Domain/Messaging/Service/Handler/DeleteUserAndBounceHandler.php similarity index 88% rename from src/Bounce/Service/Handler/DeleteUserAndBounceHandler.php rename to src/Domain/Messaging/Service/Handler/DeleteUserAndBounceHandler.php index f51dcdca..8fdc8a26 100644 --- a/src/Bounce/Service/Handler/DeleteUserAndBounceHandler.php +++ b/src/Domain/Messaging/Service/Handler/DeleteUserAndBounceHandler.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Handler; +namespace PhpList\Core\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager; use PhpList\Core\Domain\Messaging\Model\BounceAction; diff --git a/src/Bounce/Service/Handler/DeleteUserHandler.php b/src/Domain/Messaging/Service/Handler/DeleteUserHandler.php similarity index 94% rename from src/Bounce/Service/Handler/DeleteUserHandler.php rename to src/Domain/Messaging/Service/Handler/DeleteUserHandler.php index ce74894b..b340596f 100644 --- a/src/Bounce/Service/Handler/DeleteUserHandler.php +++ b/src/Domain/Messaging/Service/Handler/DeleteUserHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Handler; +namespace PhpList\Core\Domain\Messaging\Service\Handler; use PhpList\Core\Domain\Messaging\Model\BounceAction; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager; diff --git a/src/Bounce/Service/Handler/UnconfirmUserAndDeleteBounceHandler.php b/src/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandler.php similarity index 93% rename from src/Bounce/Service/Handler/UnconfirmUserAndDeleteBounceHandler.php rename to src/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandler.php index 6d191a8f..59908d23 100644 --- a/src/Bounce/Service/Handler/UnconfirmUserAndDeleteBounceHandler.php +++ b/src/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandler.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Handler; +namespace PhpList\Core\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Messaging\Model\BounceAction; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; diff --git a/src/Bounce/Service/Handler/UnconfirmUserHandler.php b/src/Domain/Messaging/Service/Handler/UnconfirmUserHandler.php similarity index 96% rename from src/Bounce/Service/Handler/UnconfirmUserHandler.php rename to src/Domain/Messaging/Service/Handler/UnconfirmUserHandler.php index 979bb20c..b0b6e2a8 100644 --- a/src/Bounce/Service/Handler/UnconfirmUserHandler.php +++ b/src/Domain/Messaging/Service/Handler/UnconfirmUserHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Handler; +namespace PhpList\Core\Domain\Messaging\Service\Handler; use PhpList\Core\Domain\Messaging\Model\BounceAction; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; diff --git a/src/Bounce/Service/LockService.php b/src/Domain/Messaging/Service/LockService.php similarity index 99% rename from src/Bounce/Service/LockService.php rename to src/Domain/Messaging/Service/LockService.php index a875959c..f4a47f34 100644 --- a/src/Bounce/Service/LockService.php +++ b/src/Domain/Messaging/Service/LockService.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service; +namespace PhpList\Core\Domain\Messaging\Service; use PhpList\Core\Domain\Messaging\Repository\SendProcessRepository; use PhpList\Core\Domain\Messaging\Service\Manager\SendProcessManager; diff --git a/src/Domain/Messaging/Service/MailSizeChecker.php b/src/Domain/Messaging/Service/MailSizeChecker.php index 9d8c9a92..e5f0de6e 100644 --- a/src/Domain/Messaging/Service/MailSizeChecker.php +++ b/src/Domain/Messaging/Service/MailSizeChecker.php @@ -14,7 +14,7 @@ class MailSizeChecker { - private ?int $maxMailSize; + private int $maxMailSize; public function __construct( private readonly EventLogManager $eventLogManager, diff --git a/src/Bounce/Service/Manager/BounceManager.php b/src/Domain/Messaging/Service/Manager/BounceManager.php similarity index 98% rename from src/Bounce/Service/Manager/BounceManager.php rename to src/Domain/Messaging/Service/Manager/BounceManager.php index 868aae10..bae5e094 100644 --- a/src/Bounce/Service/Manager/BounceManager.php +++ b/src/Domain/Messaging/Service/Manager/BounceManager.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Manager; +namespace PhpList\Core\Domain\Messaging\Service\Manager; use DateTime; use DateTimeImmutable; diff --git a/src/Domain/Messaging/Service/Manager/BounceRuleManager.php b/src/Domain/Messaging/Service/Manager/BounceRuleManager.php index 67d97d18..6dcd10aa 100644 --- a/src/Domain/Messaging/Service/Manager/BounceRuleManager.php +++ b/src/Domain/Messaging/Service/Manager/BounceRuleManager.php @@ -99,7 +99,7 @@ public function incrementCount(BounceRegex $rule): void $this->repository->save($rule); } - public function linkRuleToBounce(BounceRegex $rule, Bounce $bounce): BounceregexBounce + public function linkRuleToBounce(BounceRegex $rule, Bounce $bounce): BounceRegexBounce { $relation = new BounceRegexBounce($rule->getId(), $bounce->getId()); $this->bounceRelationRepository->save($relation); diff --git a/src/Domain/Messaging/Service/Manager/SendProcessManager.php b/src/Domain/Messaging/Service/Manager/SendProcessManager.php index 6cfacce4..082fe9a4 100644 --- a/src/Domain/Messaging/Service/Manager/SendProcessManager.php +++ b/src/Domain/Messaging/Service/Manager/SendProcessManager.php @@ -45,7 +45,7 @@ public function findNewestAliveWithAge(string $page): ?array } $modified = $row->getUpdatedAt(); - $age = $modified ? max(0, time() - (int)$modified->format('U')) : 0; + $age = max(0, time() - (int)$modified->format('U')); return [ 'id' => $row->getId(), diff --git a/src/Domain/Messaging/Service/Manager/TemplateManager.php b/src/Domain/Messaging/Service/Manager/TemplateManager.php index cca54eec..06f21ef9 100644 --- a/src/Domain/Messaging/Service/Manager/TemplateManager.php +++ b/src/Domain/Messaging/Service/Manager/TemplateManager.php @@ -31,9 +31,7 @@ public function create(CreateTemplateDto $createTemplateDto): Template ->setListOrder($createTemplateDto->listOrder); $content = $createTemplateDto->fileContent ?? $createTemplateDto->content; - if ($content !== null) { - $template->setContent($content); - } + $template->setContent($content); $context = (new ValidationContext()) ->set('checkLinks', $createTemplateDto->shouldCheckLinks) diff --git a/src/Domain/Messaging/Service/MessageForwardService.php b/src/Domain/Messaging/Service/MessageForwardService.php index 7e086a0f..b93f5d76 100644 --- a/src/Domain/Messaging/Service/MessageForwardService.php +++ b/src/Domain/Messaging/Service/MessageForwardService.php @@ -74,7 +74,11 @@ public function forward(MessageForwardDto $messageForwardDto, Message $campaign) friendEmail: $friendEmail, forwardDto: $messageForwardDto, ); - } catch (EmailBlacklistedException | MessageCacheMissingException | InvalidRecipientOrSubjectException $e) { + } catch (MessageCacheMissingException + | EmailBlacklistedException + | InvalidRecipientOrSubjectException $e + ) { + // todo: check if need to catch MessageCacheMissingException $forwardingRecipientResult = $this->handleFailure( campaign: $campaign, forwardingSubscriber: $forwardingSubscriber, diff --git a/src/Bounce/Service/MessageParser.php b/src/Domain/Messaging/Service/MessageParser.php similarity index 98% rename from src/Bounce/Service/MessageParser.php rename to src/Domain/Messaging/Service/MessageParser.php index 336cbe02..14b4f952 100644 --- a/src/Bounce/Service/MessageParser.php +++ b/src/Domain/Messaging/Service/MessageParser.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service; +namespace PhpList\Core\Domain\Messaging\Service; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; diff --git a/src/Bounce/Service/NativeBounceProcessingService.php b/src/Domain/Messaging/Service/NativeBounceProcessingService.php similarity index 94% rename from src/Bounce/Service/NativeBounceProcessingService.php rename to src/Domain/Messaging/Service/NativeBounceProcessingService.php index 887aa94d..b58f771a 100644 --- a/src/Bounce/Service/NativeBounceProcessingService.php +++ b/src/Domain/Messaging/Service/NativeBounceProcessingService.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service; +namespace PhpList\Core\Domain\Messaging\Service; use Doctrine\ORM\EntityManagerInterface; use IMAP\Connection; -use PhpList\Core\Bounce\Exception\OpenMboxFileException; -use PhpList\Core\Bounce\Service\Manager\BounceManager; -use PhpList\Core\Bounce\Service\Processor\BounceDataProcessor; +use PhpList\Core\Domain\Messaging\Exception\OpenMboxFileException; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Processor\BounceDataProcessor; use PhpList\Core\Domain\Common\Mail\NativeImapMailReader; use Psr\Log\LoggerInterface; use Throwable; diff --git a/src/Bounce/Service/Processor/AdvancedBounceRulesProcessor.php b/src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php similarity index 95% rename from src/Bounce/Service/Processor/AdvancedBounceRulesProcessor.php rename to src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php index 3d6fb116..1b703832 100644 --- a/src/Bounce/Service/Processor/AdvancedBounceRulesProcessor.php +++ b/src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Processor; +namespace PhpList\Core\Domain\Messaging\Service\Processor; -use PhpList\Core\Bounce\Service\BounceActionResolver; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\BounceActionResolver; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Messaging\Service\Manager\BounceRuleManager; use PhpList\Core\Domain\Subscription\Model\Subscriber; diff --git a/src/Bounce/Service/Processor/BounceDataProcessor.php b/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php similarity index 98% rename from src/Bounce/Service/Processor/BounceDataProcessor.php rename to src/Domain/Messaging/Service/Processor/BounceDataProcessor.php index d40707b3..3ddff5a5 100644 --- a/src/Bounce/Service/Processor/BounceDataProcessor.php +++ b/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Processor; +namespace PhpList\Core\Domain\Messaging\Service\Processor; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Messaging\Model\BounceStatus; use PhpList\Core\Domain\Messaging\Repository\MessageRepository; diff --git a/src/Bounce/Service/Processor/BounceProtocolProcessor.php b/src/Domain/Messaging/Service/Processor/BounceProtocolProcessor.php similarity index 91% rename from src/Bounce/Service/Processor/BounceProtocolProcessor.php rename to src/Domain/Messaging/Service/Processor/BounceProtocolProcessor.php index 6bb77a49..a0e7d904 100644 --- a/src/Bounce/Service/Processor/BounceProtocolProcessor.php +++ b/src/Domain/Messaging/Service/Processor/BounceProtocolProcessor.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Processor; +namespace PhpList\Core\Domain\Messaging\Service\Processor; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Style\SymfonyStyle; diff --git a/src/Bounce/Service/Processor/MboxBounceProcessor.php b/src/Domain/Messaging/Service/Processor/MboxBounceProcessor.php similarity index 91% rename from src/Bounce/Service/Processor/MboxBounceProcessor.php rename to src/Domain/Messaging/Service/Processor/MboxBounceProcessor.php index b3f8c79b..d61742d5 100644 --- a/src/Bounce/Service/Processor/MboxBounceProcessor.php +++ b/src/Domain/Messaging/Service/Processor/MboxBounceProcessor.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Processor; +namespace PhpList\Core\Domain\Messaging\Service\Processor; -use PhpList\Core\Bounce\Service\BounceProcessingServiceInterface; +use PhpList\Core\Domain\Messaging\Service\BounceProcessingServiceInterface; use RuntimeException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Style\SymfonyStyle; diff --git a/src/Bounce/Service/Processor/PopBounceProcessor.php b/src/Domain/Messaging/Service/Processor/PopBounceProcessor.php similarity index 93% rename from src/Bounce/Service/Processor/PopBounceProcessor.php rename to src/Domain/Messaging/Service/Processor/PopBounceProcessor.php index 9ebb26c4..b0079774 100644 --- a/src/Bounce/Service/Processor/PopBounceProcessor.php +++ b/src/Domain/Messaging/Service/Processor/PopBounceProcessor.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Processor; +namespace PhpList\Core\Domain\Messaging\Service\Processor; -use PhpList\Core\Bounce\Service\BounceProcessingServiceInterface; +use PhpList\Core\Domain\Messaging\Service\BounceProcessingServiceInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Contracts\Translation\TranslatorInterface; diff --git a/src/Bounce/Service/Processor/UnidentifiedBounceReprocessor.php b/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php similarity index 93% rename from src/Bounce/Service/Processor/UnidentifiedBounceReprocessor.php rename to src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php index 416684b2..b3705487 100644 --- a/src/Bounce/Service/Processor/UnidentifiedBounceReprocessor.php +++ b/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service\Processor; +namespace PhpList\Core\Domain\Messaging\Service\Processor; use DateTimeImmutable; -use PhpList\Core\Bounce\Service\Manager\BounceManager; -use PhpList\Core\Bounce\Service\MessageParser; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\MessageParser; use PhpList\Core\Domain\Messaging\Model\BounceStatus; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Contracts\Translation\TranslatorInterface; diff --git a/src/Bounce/Service/SubscriberBlacklistService.php b/src/Domain/Messaging/Service/SubscriberBlacklistService.php similarity index 96% rename from src/Bounce/Service/SubscriberBlacklistService.php rename to src/Domain/Messaging/Service/SubscriberBlacklistService.php index 38d37e7d..af8c7552 100644 --- a/src/Bounce/Service/SubscriberBlacklistService.php +++ b/src/Domain/Messaging/Service/SubscriberBlacklistService.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service; +namespace PhpList\Core\Domain\Messaging\Service; use Doctrine\ORM\EntityManagerInterface; use PhpList\Core\Domain\Subscription\Model\Subscriber; @@ -34,7 +34,7 @@ public function __construct( } /** - * @SuppressWarnings(PHPMD.Superglobals) + * @SuppressWarnings("PHPMD.Superglobals") */ public function blacklist(Subscriber $subscriber, string $reason): void { diff --git a/src/Bounce/Service/WebklexBounceProcessingService.php b/src/Domain/Messaging/Service/WebklexBounceProcessingService.php similarity index 96% rename from src/Bounce/Service/WebklexBounceProcessingService.php rename to src/Domain/Messaging/Service/WebklexBounceProcessingService.php index 4ca20461..c489585c 100644 --- a/src/Bounce/Service/WebklexBounceProcessingService.php +++ b/src/Domain/Messaging/Service/WebklexBounceProcessingService.php @@ -2,20 +2,20 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service; +namespace PhpList\Core\Domain\Messaging\Service; use DateTimeImmutable; use DateTimeInterface; -use PhpList\Core\Bounce\Exception\ImapConnectionException; -use PhpList\Core\Bounce\Service\Manager\BounceManager; -use PhpList\Core\Bounce\Service\Processor\BounceDataProcessor; +use PhpList\Core\Domain\Messaging\Exception\ImapConnectionException; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Processor\BounceDataProcessor; use Psr\Log\LoggerInterface; use Throwable; use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\Folder; /** - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings("PHPMD.ExcessiveClassComplexity") */ class WebklexBounceProcessingService implements BounceProcessingServiceInterface { diff --git a/src/Bounce/Service/WebklexImapClientFactory.php b/src/Domain/Messaging/Service/WebklexImapClientFactory.php similarity index 97% rename from src/Bounce/Service/WebklexImapClientFactory.php rename to src/Domain/Messaging/Service/WebklexImapClientFactory.php index 48fc26bc..10271e4c 100644 --- a/src/Bounce/Service/WebklexImapClientFactory.php +++ b/src/Domain/Messaging/Service/WebklexImapClientFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Bounce\Service; +namespace PhpList\Core\Domain\Messaging\Service; use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\ClientManager; diff --git a/src/Domain/Subscription/Exception/AttributeDefinitionCreationException.php b/src/Domain/Subscription/Exception/AttributeDefinitionCreationException.php deleted file mode 100644 index 2ea5ef45..00000000 --- a/src/Domain/Subscription/Exception/AttributeDefinitionCreationException.php +++ /dev/null @@ -1,23 +0,0 @@ -statusCode = $statusCode; - } - - public function getStatusCode(): int - { - return $this->statusCode; - } -} diff --git a/src/Domain/Subscription/Model/SubscribePage.php b/src/Domain/Subscription/Model/SubscribePage.php index 3b484920..bc4ea54f 100644 --- a/src/Domain/Subscription/Model/SubscribePage.php +++ b/src/Domain/Subscription/Model/SubscribePage.php @@ -12,7 +12,7 @@ use PhpList\Core\Domain\Subscription\Repository\SubscriberPageRepository; #[ORM\Entity(repositoryClass: SubscriberPageRepository::class)] -#[ORM\Table(name: 'phplist_subscribepage')] +#[ORM\Table(name: 'subscribepage')] class SubscribePage implements DomainModel, Identity, OwnableInterface { #[ORM\Id] diff --git a/src/Domain/Subscription/Model/SubscribePageData.php b/src/Domain/Subscription/Model/SubscribePageData.php index 7d8dcd4e..8b94e729 100644 --- a/src/Domain/Subscription/Model/SubscribePageData.php +++ b/src/Domain/Subscription/Model/SubscribePageData.php @@ -9,7 +9,7 @@ use PhpList\Core\Domain\Subscription\Repository\SubscriberPageDataRepository; #[ORM\Entity(repositoryClass: SubscriberPageDataRepository::class)] -#[ORM\Table(name: 'phplist_subscribepage_data')] +#[ORM\Table(name: 'subscribepage_data')] class SubscribePageData implements DomainModel { #[ORM\Id] diff --git a/src/Domain/Subscription/Model/Subscriber.php b/src/Domain/Subscription/Model/Subscriber.php index 97d45b83..53299540 100644 --- a/src/Domain/Subscription/Model/Subscriber.php +++ b/src/Domain/Subscription/Model/Subscriber.php @@ -19,12 +19,12 @@ * campaigns for those subscriber lists. * @author Oliver Klee * @author Tatevik Grigoryan - * @SuppressWarnings(TooManyFields) - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings("TooManyFields") + * @SuppressWarnings("PHPMD.ExcessiveClassComplexity") + * @SuppressWarnings("PHPMD.ExcessivePublicCount") */ #[ORM\Entity(repositoryClass: SubscriberRepository::class)] -#[ORM\Table(name: 'phplist_user_user')] +#[ORM\Table(name: 'user_user')] #[ORM\Index(name: 'phplist_user_user_idxuniqid', columns: ['uniqid'])] #[ORM\Index(name: 'phplist_user_user_enteredindex', columns: ['entered'])] #[ORM\Index(name: 'phplist_user_user_confidx', columns: ['confirmed'])] @@ -45,7 +45,7 @@ class Subscriber implements DomainModel, Identity, CreationDate, ModificationDat protected ?DateTime $createdAt = null; #[ORM\Column(name: 'modified', type: 'datetime', nullable: false)] - private ?DateTime $updatedAt = null; + private DateTime $updatedAt; #[ORM\Column(unique: true)] private string $email = ''; @@ -59,7 +59,7 @@ class Subscriber implements DomainModel, Identity, CreationDate, ModificationDat #[ORM\Column(name: 'bouncecount', type: 'integer')] private int $bounceCount = 0; - #[ORM\Column(name: 'uniqid', type: 'string', length: 255, nullable: true)] + #[ORM\Column(name: 'uniqid', type: 'string', length: 255)] private string $uniqueId = ''; #[ORM\Column(name: 'htmlemail', type: 'boolean')] @@ -71,6 +71,9 @@ class Subscriber implements DomainModel, Identity, CreationDate, ModificationDat #[ORM\Column(name: 'extradata', type: 'text', nullable: true)] private ?string $extraData = null; + /** + * @var Collection + */ #[ORM\OneToMany( targetEntity: Subscription::class, mappedBy: 'subscriber', @@ -134,7 +137,7 @@ public function getCreatedAt(): ?DateTime return $this->createdAt; } - public function getUpdatedAt(): ?DateTime + public function getUpdatedAt(): DateTime { return $this->updatedAt; } @@ -257,7 +260,7 @@ public function setExtraData(?string $extraData): self } /** - * @return Collection + * @return Collection */ public function getSubscriptions(): Collection { @@ -274,15 +277,6 @@ public function addSubscription(Subscription $subscription): self return $this; } - public function removeSubscription(Subscription $subscription): self - { - if ($this->subscriptions->removeElement($subscription)) { - $subscription->setSubscriber(null); - } - - return $this; - } - public function getSubscribedLists(): Collection { $result = new ArrayCollection(); diff --git a/src/Domain/Subscription/Model/SubscriberAttributeDefinition.php b/src/Domain/Subscription/Model/SubscriberAttributeDefinition.php index 26b7a786..dbe397d2 100644 --- a/src/Domain/Subscription/Model/SubscriberAttributeDefinition.php +++ b/src/Domain/Subscription/Model/SubscriberAttributeDefinition.php @@ -12,7 +12,7 @@ use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; #[ORM\Entity(repositoryClass: SubscriberAttributeDefinitionRepository::class)] -#[ORM\Table(name: 'phplist_user_attribute')] +#[ORM\Table(name: 'user_attribute')] #[ORM\Index(name: 'phplist_user_attribute_idnameindex', columns: ['id', 'name'])] #[ORM\Index(name: 'phplist_user_attribute_nameindex', columns: ['name'])] class SubscriberAttributeDefinition implements DomainModel, Identity diff --git a/src/Domain/Subscription/Model/SubscriberAttributeValue.php b/src/Domain/Subscription/Model/SubscriberAttributeValue.php index 3af333ff..6678b489 100644 --- a/src/Domain/Subscription/Model/SubscriberAttributeValue.php +++ b/src/Domain/Subscription/Model/SubscriberAttributeValue.php @@ -9,7 +9,7 @@ use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeValueRepository; #[ORM\Entity(repositoryClass: SubscriberAttributeValueRepository::class)] -#[ORM\Table(name: 'phplist_user_user_attribute')] +#[ORM\Table(name: 'user_user_attribute')] #[ORM\Index(name: 'phplist_user_user_attribute_attindex', columns: ['attributeid'])] #[ORM\Index(name: 'phplist_user_user_attribute_attuserid', columns: ['userid', 'attributeid'])] #[ORM\Index(name: 'phplist_user_user_attribute_userindex', columns: ['userid'])] diff --git a/src/Domain/Subscription/Model/SubscriberHistory.php b/src/Domain/Subscription/Model/SubscriberHistory.php index 1799c01b..08f4f974 100644 --- a/src/Domain/Subscription/Model/SubscriberHistory.php +++ b/src/Domain/Subscription/Model/SubscriberHistory.php @@ -11,7 +11,7 @@ use PhpList\Core\Domain\Subscription\Repository\SubscriberHistoryRepository; #[ORM\Entity(repositoryClass: SubscriberHistoryRepository::class)] -#[ORM\Table(name: 'phplist_user_user_history')] +#[ORM\Table(name: 'user_user_history')] #[ORM\Index(name: 'phplist_user_user_history_dateidx', columns: ['date'])] #[ORM\Index(name: 'phplist_user_user_history_userididx', columns: ['userid'])] class SubscriberHistory implements DomainModel, Identity diff --git a/src/Domain/Subscription/Model/SubscriberList.php b/src/Domain/Subscription/Model/SubscriberList.php index 621f855e..d62bd872 100644 --- a/src/Domain/Subscription/Model/SubscriberList.php +++ b/src/Domain/Subscription/Model/SubscriberList.php @@ -25,7 +25,7 @@ * @author Tatevik Grigoryan */ #[ORM\Entity(repositoryClass: SubscriberListRepository::class)] -#[ORM\Table(name: 'phplist_list')] +#[ORM\Table(name: 'list')] #[ORM\Index(name: 'phplist_list_nameidx', columns: ['name'])] #[ORM\Index(name: 'phplist_list_listorderidx', columns: ['listorder'])] #[ORM\HasLifecycleCallbacks] @@ -42,14 +42,14 @@ class SubscriberList implements DomainModel, Identity, CreationDate, Modificatio #[ORM\Column(name: 'rssfeed', type: 'string', length: 255, nullable: true)] private ?string $rssFeed = null; - #[ORM\Column] - private ?string $description = ''; + #[ORM\Column(nullable: true)] + private ?string $description = null; #[ORM\Column(name: 'entered', type: 'datetime', nullable: true)] protected ?DateTime $createdAt = null; #[ORM\Column(name: 'modified', type: 'datetime')] - private ?DateTime $updatedAt = null; + private DateTime $updatedAt; #[ORM\Column(name: 'listorder', type: 'integer', nullable: true)] private ?int $listPosition; @@ -60,13 +60,16 @@ class SubscriberList implements DomainModel, Identity, CreationDate, Modificatio #[ORM\Column(name: 'active', type: 'boolean')] private bool $public; - #[ORM\Column] - private ?string $category = ''; + #[ORM\Column(nullable: true)] + private ?string $category = null; #[ORM\ManyToOne(targetEntity: Administrator::class, inversedBy: 'ownedLists')] #[ORM\JoinColumn(name: 'owner')] private ?Administrator $owner = null; + /** + * @var Collection + */ #[ORM\OneToMany( targetEntity: Subscription::class, mappedBy: 'subscriberList', @@ -76,6 +79,9 @@ class SubscriberList implements DomainModel, Identity, CreationDate, Modificatio #[MaxDepth(1)] private Collection $subscriptions; + /** + * @var Collection + */ #[ORM\OneToMany(targetEntity: ListMessage::class, mappedBy: 'subscriberList')] private Collection $listMessages; @@ -84,9 +90,9 @@ public function __construct() $this->subscriptions = new ArrayCollection(); $this->listMessages = new ArrayCollection(); $this->createdAt = new DateTime(); + $this->updatedAt = new DateTime(); $this->listPosition = 0; $this->subjectPrefix = ''; - $this->category = ''; $this->public = false; } @@ -154,7 +160,7 @@ public function setSubjectPrefix(?string $subjectPrefix): self public function isPublic(): bool { - return $this->public ?? false; + return $this->public; } public function setPublic(bool $public): self @@ -200,15 +206,6 @@ public function addSubscription(Subscription $subscription): self return $this; } - public function removeSubscription(Subscription $subscription): self - { - if ($this->subscriptions->removeElement($subscription)) { - $subscription->setSubscriberList(null); - } - - return $this; - } - public function getSubscribers(): Collection { $result = new ArrayCollection(); @@ -224,7 +221,7 @@ public function getCreatedAt(): ?DateTime return $this->createdAt; } - public function getUpdatedAt(): ?DateTime + public function getUpdatedAt(): DateTime { return $this->updatedAt; } diff --git a/src/Domain/Subscription/Model/Subscription.php b/src/Domain/Subscription/Model/Subscription.php index fe4b5e2a..3721d571 100644 --- a/src/Domain/Subscription/Model/Subscription.php +++ b/src/Domain/Subscription/Model/Subscription.php @@ -22,7 +22,7 @@ * @author Tatevik Grigoryan */ #[ORM\Entity(repositoryClass: SubscriptionRepository::class)] -#[ORM\Table(name: 'phplist_listuser')] +#[ORM\Table(name: 'listuser')] #[ORM\Index(name: 'phplist_listuser_userenteredidx', columns: ['userid', 'entered'])] #[ORM\Index(name: 'phplist_listuser_userlistenteredidx', columns: ['userid', 'entered', 'listid'])] #[ORM\Index(name: 'phplist_listuser_useridx', columns: ['userid'])] @@ -35,7 +35,7 @@ class Subscription implements DomainModel, CreationDate, ModificationDate protected ?DateTime $createdAt = null; #[ORM\Column(name: 'modified', type: 'datetime')] - private ?DateTime $updatedAt = null; + private DateTime $updatedAt; #[ORM\Id] #[ORM\ManyToOne( @@ -44,7 +44,7 @@ class Subscription implements DomainModel, CreationDate, ModificationDate )] #[ORM\JoinColumn(name: 'userid')] #[SerializedName('subscriber')] - private ?Subscriber $subscriber = null; + private Subscriber $subscriber; #[ORM\Id] #[ORM\ManyToOne( @@ -54,30 +54,31 @@ class Subscription implements DomainModel, CreationDate, ModificationDate #[ORM\JoinColumn(name: 'listid', onDelete: 'CASCADE')] #[Ignore] #[Groups(['SubscriberListMembers'])] - private ?SubscriberList $subscriberList = null; + private SubscriberList $subscriberList; public function __construct() { $this->createdAt = new DateTime(); + $this->updatedAt = new DateTime(); } - public function getSubscriber(): Subscriber|Proxy|null + public function getSubscriber(): Subscriber|Proxy { return $this->subscriber; } - public function setSubscriber(?Subscriber $subscriber): self + public function setSubscriber(Subscriber $subscriber): self { $this->subscriber = $subscriber; return $this; } - public function getSubscriberList(): ?SubscriberList + public function getSubscriberList(): SubscriberList|Proxy { return $this->subscriberList; } - public function setSubscriberList(?SubscriberList $subscriberList): self + public function setSubscriberList(SubscriberList $subscriberList): self { $this->subscriberList = $subscriberList; return $this; @@ -88,7 +89,7 @@ public function getCreatedAt(): ?DateTime return $this->createdAt; } - public function getUpdatedAt(): ?DateTime + public function getUpdatedAt(): DateTime { return $this->updatedAt; } diff --git a/src/Domain/Subscription/Model/UserBlacklist.php b/src/Domain/Subscription/Model/UserBlacklist.php index 9b150686..f940f79b 100644 --- a/src/Domain/Subscription/Model/UserBlacklist.php +++ b/src/Domain/Subscription/Model/UserBlacklist.php @@ -10,7 +10,7 @@ use PhpList\Core\Domain\Subscription\Repository\UserBlacklistRepository; #[ORM\Entity(repositoryClass: UserBlacklistRepository::class)] -#[ORM\Table(name: 'phplist_user_blacklist')] +#[ORM\Table(name: 'user_blacklist')] #[ORM\Index(name: 'phplist_user_blacklist_emailidx', columns: ['email'])] class UserBlacklist implements DomainModel { diff --git a/src/Domain/Subscription/Model/UserBlacklistData.php b/src/Domain/Subscription/Model/UserBlacklistData.php index ff133161..52725e1b 100644 --- a/src/Domain/Subscription/Model/UserBlacklistData.php +++ b/src/Domain/Subscription/Model/UserBlacklistData.php @@ -9,7 +9,7 @@ use PhpList\Core\Domain\Subscription\Repository\UserBlacklistDataRepository; #[ORM\Entity(repositoryClass: UserBlacklistDataRepository::class)] -#[ORM\Table(name: 'phplist_user_blacklist_data')] +#[ORM\Table(name: 'user_blacklist_data')] #[ORM\Index(name: 'phplist_user_blacklist_data_emailidx', columns: ['email'])] #[ORM\Index(name: 'phplist_user_blacklist_data_emailnameidx', columns: ['email', 'name'])] class UserBlacklistData implements DomainModel diff --git a/src/Domain/Subscription/Repository/DynamicListAttrRepository.php b/src/Domain/Subscription/Repository/DynamicListAttrRepository.php index 25dbcf34..219345b4 100644 --- a/src/Domain/Subscription/Repository/DynamicListAttrRepository.php +++ b/src/Domain/Subscription/Repository/DynamicListAttrRepository.php @@ -6,8 +6,8 @@ use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\ParameterType; use InvalidArgumentException; -use PDO; use PhpList\Core\Domain\Subscription\Model\Dto\DynamicListAttrDto; use Symfony\Component\Serializer\Exception\ExceptionInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -161,12 +161,12 @@ public function isNameTakenByOtherRecord(string $listTable, DynamicListAttrDto $ $sql = 'SELECT 1 FROM ' . $table . ' WHERE LOWER(name) = LOWER(:name)'; $params = ['name' => $dto->name]; - $types = ['name' => PDO::PARAM_STR]; + $types = ['name' => ParameterType::STRING]; if ($dto->id !== null) { $sql .= ' AND id <> :excludeId'; $params['excludeId'] = $dto->id; - $types['excludeId'] = PDO::PARAM_INT; + $types['excludeId'] = ParameterType::INTEGER; } $sql .= ' LIMIT 1'; diff --git a/src/Domain/Subscription/Repository/SubscriberPageRepository.php b/src/Domain/Subscription/Repository/SubscriberPageRepository.php index c9f39449..7556b85e 100644 --- a/src/Domain/Subscription/Repository/SubscriberPageRepository.php +++ b/src/Domain/Subscription/Repository/SubscriberPageRepository.php @@ -65,12 +65,9 @@ public function getFilteredAfterId(FilterRequestInterface $filter): PaginatedRes $grouped = []; foreach ($rows as $row) { - /** @var SubscribePage $page */ - $page = $row['page'] ?? null; + $page = $row['page']; $data = $row['data'] ?? null; - if ($page !== null) { - $grouped[$page->getId()][] = $row; - } + $grouped[$page->getId()][] = $row; if ($data !== null) { $grouped[$data->getId()][] = ['data' => $data]; } @@ -82,7 +79,7 @@ public function getFilteredAfterId(FilterRequestInterface $filter): PaginatedRes } return new PaginatedResult( - items: array_values($pages), + items: $pages, total: $total, limit: $filter->getLimit(), lastId: $filter->getLastId(), diff --git a/src/Domain/Subscription/Repository/SubscriberRepository.php b/src/Domain/Subscription/Repository/SubscriberRepository.php index 4fbfac0a..e9def63e 100644 --- a/src/Domain/Subscription/Repository/SubscriberRepository.php +++ b/src/Domain/Subscription/Repository/SubscriberRepository.php @@ -19,7 +19,7 @@ * * @author Oliver Klee * @author Tatevik Grigoryan - * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings("PHPMD.TooManyPublicMethods") */ class SubscriberRepository extends AbstractRepository implements PaginatableRepositoryInterface { @@ -343,4 +343,81 @@ public function getByEmails(array $emails): array ->getQuery() ->getResult(); } + + /** + * Returns the top domains (by subscriber count) among subscribers with a valid email address. + * Aggregation happens in SQL so only $limit rows are ever loaded into memory. + * + * @return array + */ + public function getTopDomains(int $limit, int $minSubscribers): array + { + return $this->createQueryBuilder('s') + ->select("SUBSTRING(s.email, LOCATE('@', s.email) + 1, LENGTH(s.email)) AS domain") + ->addSelect('COUNT(s.id) AS subscribers') + ->where("LOCATE('@', s.email) > 0") + ->groupBy('domain') + ->having('COUNT(s.id) >= :minSubscribers') + ->setParameter('minSubscribers', $minSubscribers) + ->orderBy('subscribers', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getArrayResult(); + } + + /** + * Returns per-domain confirmed/unconfirmed/blacklisted subscriber counts, ordered by unconfirmed count. + * Aggregation happens in SQL so only $limit rows are ever loaded into memory. + * + * @return array + */ + public function getDomainConfirmationStatistics(int $limit): array + { + return $this->createQueryBuilder('s') + ->select("SUBSTRING(s.email, LOCATE('@', s.email) + 1, LENGTH(s.email)) AS domain") + ->addSelect('COUNT(s.id) AS total') + ->addSelect('SUM(CASE WHEN s.blacklisted = true THEN 1 ELSE 0 END) AS blacklisted') + ->addSelect('SUM(CASE WHEN s.blacklisted = false AND s.confirmed = true THEN 1 ELSE 0 END) AS confirmed') + ->addSelect( + 'SUM(CASE WHEN s.blacklisted = false AND s.confirmed = false THEN 1 ELSE 0 END) AS unconfirmed' + ) + ->where("LOCATE('@', s.email) > 0") + ->groupBy('domain') + ->orderBy('unconfirmed', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getArrayResult(); + } + + /** + * Returns the top local-parts (by subscriber count) among subscribers with a valid email address. + * Aggregation happens in SQL so only $limit rows are ever loaded into memory. + * + * @return array + */ + public function getTopLocalParts(int $limit): array + { + return $this->createQueryBuilder('s') + ->select("SUBSTRING(s.email, 1, LOCATE('@', s.email) - 1) AS localPart") + ->addSelect('COUNT(s.id) AS count') + ->where("LOCATE('@', s.email) > 0") + ->groupBy('localPart') + ->orderBy('count', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getArrayResult(); + } + + /** + * Counts subscribers whose email address contains an '@'. + */ + public function countWithValidEmail(): int + { + return (int) $this->createQueryBuilder('s') + ->select('COUNT(s.id)') + ->where("LOCATE('@', s.email) > 0") + ->getQuery() + ->getSingleScalarResult(); + } } diff --git a/src/Domain/Subscription/Service/Manager/AttributeDefinitionManager.php b/src/Domain/Subscription/Service/Manager/AttributeDefinitionManager.php index 20159766..bfc809f1 100644 --- a/src/Domain/Subscription/Service/Manager/AttributeDefinitionManager.php +++ b/src/Domain/Subscription/Service/Manager/AttributeDefinitionManager.php @@ -4,7 +4,7 @@ namespace PhpList\Core\Domain\Subscription\Service\Manager; -use PhpList\Core\Domain\Subscription\Exception\AttributeDefinitionCreationException; +use PhpList\Core\Domain\Common\Exception\AttributeDefinitionCreationException; use PhpList\Core\Domain\Subscription\Model\Dto\AttributeDefinitionDto; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; diff --git a/src/Domain/Subscription/Service/Manager/DynamicListAttrManager.php b/src/Domain/Subscription/Service/Manager/DynamicListAttrManager.php index a19d21b9..6d4d952d 100644 --- a/src/Domain/Subscription/Service/Manager/DynamicListAttrManager.php +++ b/src/Domain/Subscription/Service/Manager/DynamicListAttrManager.php @@ -54,10 +54,6 @@ public function insertOptions(string $listTable, array $rawOptions, array &$used $unique[] = $opt; } - if ($unique === []) { - return $result; - } - return $this->dynamicListAttrRepository->transactional(function () use ($listTable, $unique) { return $this->dynamicListAttrRepository->insertMany($listTable, $unique); }); diff --git a/src/Domain/Subscription/Service/SubscriberCsvExporter.php b/src/Domain/Subscription/Service/SubscriberCsvExporter.php index f2b1d43c..48802fe0 100644 --- a/src/Domain/Subscription/Service/SubscriberCsvExporter.php +++ b/src/Domain/Subscription/Service/SubscriberCsvExporter.php @@ -263,7 +263,7 @@ private function normalizerSubscriberData(Subscriber $subscriber): array 'blacklisted' => $subscriber->isBlacklisted() ? '1' : '0', 'bounceCount' => $subscriber->getBounceCount(), 'createdAt' => $subscriber->getCreatedAt()?->format('Y-m-d H:i:s') ?? '', - 'updatedAt' => $subscriber->getUpdatedAt()?->format('Y-m-d H:i:s') ?? '', + 'updatedAt' => $subscriber->getUpdatedAt()->format('Y-m-d H:i:s'), 'uniqueId' => $subscriber->getUniqueId(), 'htmlEmail' => $subscriber->hasHtmlEmail() ? '1' : '0', 'rssFrequency' => $subscriber->getRssFrequency(), diff --git a/src/Domain/Subscription/Validator/AttributeTypeValidator.php b/src/Domain/Subscription/Validator/AttributeTypeValidator.php index d4ededff..632db921 100644 --- a/src/Domain/Subscription/Validator/AttributeTypeValidator.php +++ b/src/Domain/Subscription/Validator/AttributeTypeValidator.php @@ -5,18 +5,10 @@ namespace PhpList\Core\Domain\Subscription\Validator; use PhpList\Core\Domain\Common\Model\AttributeTypeEnum; -use PhpList\Core\Domain\Common\Model\ValidationContext; -use PhpList\Core\Domain\Common\Validator\ValidatorInterface; -use Symfony\Component\Validator\Exception\ValidatorException; -use Symfony\Contracts\Translation\TranslatorInterface; -use Throwable; +use PhpList\Core\Domain\Common\Validator\AbstractAttributeTypeValidator; -class AttributeTypeValidator implements ValidatorInterface +class AttributeTypeValidator extends AbstractAttributeTypeValidator { - public function __construct(private readonly TranslatorInterface $translator) - { - } - private const VALID_TYPES = [ AttributeTypeEnum::TextLine, AttributeTypeEnum::Hidden, @@ -29,52 +21,8 @@ public function __construct(private readonly TranslatorInterface $translator) AttributeTypeEnum::CheckboxGroup, ]; - public function validate(mixed $value, ValidationContext $context = null): void + protected function getValidTypes(): array { - $enum = $this->normalizeToEnum($value); - - if (!in_array($enum, self::VALID_TYPES, true)) { - $validList = implode(', ', array_map( - static fn(AttributeTypeEnum $enum) => $enum->value, - self::VALID_TYPES - )); - - $message = $this->translator->trans( - 'Invalid attribute type: "%type%". Valid types are: %valid_types%', - [ - '%type%' => $enum->value, - '%valid_types%' => $validList, - ] - ); - - throw new ValidatorException($message); - } - } - - /** - * @throws ValidatorException if value cannot be converted to AttributeTypeEnum - */ - private function normalizeToEnum(mixed $value): AttributeTypeEnum - { - if ($value instanceof AttributeTypeEnum) { - return $value; - } - - if (is_string($value)) { - try { - return AttributeTypeEnum::from($value); - } catch (Throwable) { - $lower = strtolower($value); - foreach (AttributeTypeEnum::cases() as $case) { - if ($case->value === $lower) { - return $case; - } - } - } - } - - throw new ValidatorException( - $this->translator->trans('Value must be an AttributeTypeEnum or string.') - ); + return self::VALID_TYPES; } } diff --git a/src/Migrations/AbstractPrefixedMigration.php b/src/Migrations/AbstractPrefixedMigration.php new file mode 100644 index 00000000..96959f5f --- /dev/null +++ b/src/Migrations/AbstractPrefixedMigration.php @@ -0,0 +1,70 @@ +getTablePrefix(), + $sql + ), + $params, + $types + ); + } + + /** + * Legacy phpList dumps don't all carry the same set of index names (older exports predate + * some indexes entirely), so a hardcoded RENAME INDEX can fail against a given dump. This + * renames whichever of the candidate legacy names is actually present, or creates the target + * index fresh if none of them are. + */ + protected function renameOrCreateIndex( + Schema $schema, + string $tableName, + array $possibleOldIndexNames, + string $newIndexName, + array $columns + ): void { + $table = $schema->getTable($this->getPrefixedTableName($tableName)); + + foreach ($possibleOldIndexNames as $oldIndexName) { + if ($table->hasIndex($oldIndexName)) { + $this->addSql(sprintf('ALTER TABLE %s RENAME INDEX %s TO %s', $tableName, $oldIndexName, $newIndexName)); + + return; + } + } + + if (!$table->hasIndex($newIndexName)) { + $this->addSql(sprintf('CREATE INDEX %s ON %s (%s)', $newIndexName, $tableName, implode(', ', $columns))); + } + } + + private function getPrefixedTableName(string $tableName): string + { + return str_replace(self::DEFAULT_PREFIX, $this->getTablePrefix(), $tableName); + } + + private function getTablePrefix(): string + { + $prefix = $_ENV['DATABASE_PREFIX'] ?? getenv('DATABASE_PREFIX'); + + return is_string($prefix) && $prefix !== '' ? $prefix : self::DEFAULT_PREFIX; + } +} diff --git a/src/Migrations/Version20251028092901MySqlInit.php b/src/Migrations/Version20251028092901MySqlInit.php index 5589fadf..7de730c0 100644 --- a/src/Migrations/Version20251028092901MySqlInit.php +++ b/src/Migrations/Version20251028092901MySqlInit.php @@ -6,12 +6,11 @@ use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; /** * Manual Migration */ -final class Version20251028092901MySqlInit extends AbstractMigration +final class Version20251028092901MySqlInit extends AbstractPrefixedMigration { public function getDescription(): string { diff --git a/src/Migrations/Version20251028092902MySqlEngineUpdate.php b/src/Migrations/Version20251028092902MySqlEngineUpdate.php new file mode 100644 index 00000000..4175c741 --- /dev/null +++ b/src/Migrations/Version20251028092902MySqlEngineUpdate.php @@ -0,0 +1,60 @@ +connection->getDatabasePlatform(); + $this->skipIf(!$platform instanceof MySQLPlatform, sprintf( + 'Unsupported platform for this migration: %s', + get_class($platform) + )); + + $engine = $this->connection->fetchOne(" + SELECT ENGINE + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'phplist_user_user' + "); + + if ($engine !== 'InnoDB') { + // legacy phpList installs created these tables as MyISAM, which cannot be referenced by + // the InnoDB foreign keys added below (MySQL error 1824: Failed to open the referenced table) + $this->addSql('ALTER TABLE phplist_admin ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_admin_attribute ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_adminattribute ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_admintoken ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_list ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_listmessage ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_listuser ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_message ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_subscribepage ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_template ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_templateimage ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_user_attribute ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_user_blacklist ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_user_blacklist_data ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_user_user ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_user_user_attribute ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_user_user_history ENGINE=InnoDB'); + $this->addSql('ALTER TABLE phplist_usermessage ENGINE=InnoDB'); + } + } + + public function down(Schema $schema): void + { + + } +} diff --git a/src/Migrations/Version20251028092902MySqlUpdate.php b/src/Migrations/Version20251028092902MySqlUpdate.php index 2c0e872e..869915aa 100644 --- a/src/Migrations/Version20251028092902MySqlUpdate.php +++ b/src/Migrations/Version20251028092902MySqlUpdate.php @@ -6,10 +6,9 @@ use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Platforms\MySQLPlatform; -use Doctrine\Migrations\AbstractMigration; use Doctrine\DBAL\Schema\Schema; -final class Version20251028092902MySqlUpdate extends AbstractMigration +final class Version20251028092902MySqlUpdate extends AbstractPrefixedMigration { public function getDescription(): string { @@ -24,19 +23,25 @@ public function up(Schema $schema): void get_class($platform) )); + $this->addSql('UPDATE phplist_admin SET created = COALESCE(created, modified, NOW()) WHERE created IS NULL'); $this->addSql('ALTER TABLE phplist_admin CHANGE created created DATETIME NOT NULL, CHANGE modified modified DATETIME NOT NULL, CHANGE superuser superuser TINYINT(1) NOT NULL, CHANGE disabled disabled TINYINT(1) NOT NULL, CHANGE privileges privileges LONGTEXT DEFAULT NULL'); $this->addSql('ALTER TABLE phplist_admin RENAME INDEX loginnameidx TO phplist_admin_loginnameidx'); + $this->addSql('DELETE t FROM phplist_admin_attribute t LEFT JOIN phplist_adminattribute p ON t.adminattributeid = p.id WHERE p.id IS NULL'); + $this->addSql('DELETE t FROM phplist_admin_attribute t LEFT JOIN phplist_admin p ON t.adminid = p.id WHERE p.id IS NULL'); $this->addSql('ALTER TABLE phplist_admin_attribute ADD CONSTRAINT FK_58E07690D3B10C48 FOREIGN KEY (adminattributeid) REFERENCES phplist_adminattribute (id)'); $this->addSql('ALTER TABLE phplist_admin_attribute ADD CONSTRAINT FK_58E07690B8ED4D93 FOREIGN KEY (adminid) REFERENCES phplist_admin (id)'); $this->addSql('CREATE INDEX IDX_58E07690D3B10C48 ON phplist_admin_attribute (adminattributeid)'); $this->addSql('CREATE INDEX IDX_58E07690B8ED4D93 ON phplist_admin_attribute (adminid)'); $this->addSql('ALTER TABLE phplist_admin_login CHANGE active active TINYINT(1) NOT NULL'); + $this->addSql('DELETE t FROM phplist_admin_login t LEFT JOIN phplist_admin p ON t.adminid = p.id WHERE p.id IS NULL'); $this->addSql('ALTER TABLE phplist_admin_login ADD CONSTRAINT FK_5FCE0842B8ED4D93 FOREIGN KEY (adminid) REFERENCES phplist_admin (id)'); $this->addSql('CREATE INDEX IDX_5FCE0842B8ED4D93 ON phplist_admin_login (adminid)'); $this->addSql('ALTER TABLE phplist_admin_password_request CHANGE id_key id_key INT UNSIGNED AUTO_INCREMENT NOT NULL'); + $this->addSql('UPDATE phplist_admin_password_request t LEFT JOIN phplist_admin p ON t.admin = p.id SET t.admin = NULL WHERE t.admin IS NOT NULL AND p.id IS NULL'); $this->addSql('ALTER TABLE phplist_admin_password_request ADD CONSTRAINT FK_DC146F3B880E0D76 FOREIGN KEY (`admin`) REFERENCES phplist_admin (id)'); $this->addSql('CREATE INDEX IDX_DC146F3B880E0D76 ON phplist_admin_password_request (`admin`)'); $this->addSql('ALTER TABLE phplist_admintoken CHANGE adminid adminid INT DEFAULT NULL, CHANGE value value VARCHAR(255) NOT NULL'); + $this->addSql('UPDATE phplist_admintoken t LEFT JOIN phplist_admin p ON t.adminid = p.id SET t.adminid = NULL WHERE t.adminid IS NOT NULL AND p.id IS NULL'); $this->addSql('ALTER TABLE phplist_admintoken ADD CONSTRAINT FK_CB15D477B8ED4D93 FOREIGN KEY (adminid) REFERENCES phplist_admin (id) ON DELETE CASCADE'); $this->addSql('CREATE INDEX IDX_CB15D477B8ED4D93 ON phplist_admintoken (adminid)'); $this->addSql('ALTER TABLE phplist_attachment CHANGE description description LONGTEXT DEFAULT NULL'); @@ -72,11 +77,14 @@ public function up(Schema $schema): void $this->addSql('ALTER TABLE phplist_linktrack_userclick RENAME INDEX midindex TO phplist_linktrack_userclick_midindex'); $this->addSql('ALTER TABLE phplist_linktrack_userclick RENAME INDEX uidindex TO phplist_linktrack_userclick_uidindex'); $this->addSql('ALTER TABLE phplist_list CHANGE description description VARCHAR(255) NOT NULL, CHANGE modified modified DATETIME NOT NULL, CHANGE active active TINYINT(1) NOT NULL, CHANGE category category VARCHAR(255) NOT NULL'); + $this->addSql('UPDATE phplist_list t LEFT JOIN phplist_admin p ON t.owner = p.id SET t.owner = NULL WHERE t.owner IS NOT NULL AND p.id IS NULL'); $this->addSql('ALTER TABLE phplist_list ADD CONSTRAINT FK_A4CE8621CF60E67C FOREIGN KEY (owner) REFERENCES phplist_admin (id)'); $this->addSql('CREATE INDEX IDX_A4CE8621CF60E67C ON phplist_list (owner)'); $this->addSql('ALTER TABLE phplist_list RENAME INDEX nameidx TO phplist_list_nameidx'); $this->addSql('ALTER TABLE phplist_list RENAME INDEX listorderidx TO phplist_list_listorderidx'); $this->addSql('ALTER TABLE phplist_listmessage CHANGE modified modified DATETIME NOT NULL'); + $this->addSql('DELETE t FROM phplist_listmessage t LEFT JOIN phplist_message p ON t.messageid = p.id WHERE p.id IS NULL'); + $this->addSql('DELETE t FROM phplist_listmessage t LEFT JOIN phplist_list p ON t.listid = p.id WHERE p.id IS NULL'); $this->addSql('ALTER TABLE phplist_listmessage ADD CONSTRAINT FK_83B22D7A31478478 FOREIGN KEY (messageid) REFERENCES phplist_message (id)'); $this->addSql('ALTER TABLE phplist_listmessage ADD CONSTRAINT FK_83B22D7A8E44C1EF FOREIGN KEY (listid) REFERENCES phplist_list (id)'); $this->addSql('CREATE INDEX IDX_83B22D7A31478478 ON phplist_listmessage (messageid)'); @@ -85,13 +93,17 @@ public function up(Schema $schema): void $this->addSql('ALTER TABLE phplist_listmessage RENAME INDEX messageid TO phplist_listmessage_messageid'); $this->addSql('DROP INDEX userlistenteredidx ON phplist_listuser'); $this->addSql('ALTER TABLE phplist_listuser CHANGE modified modified DATETIME NOT NULL'); + $this->addSql('DELETE t FROM phplist_listuser t LEFT JOIN phplist_user_user p ON t.userid = p.id WHERE p.id IS NULL'); + $this->addSql('DELETE t FROM phplist_listuser t LEFT JOIN phplist_list p ON t.listid = p.id WHERE p.id IS NULL'); $this->addSql('ALTER TABLE phplist_listuser ADD CONSTRAINT FK_F467E411F132696E FOREIGN KEY (userid) REFERENCES phplist_user_user (id)'); $this->addSql('ALTER TABLE phplist_listuser ADD CONSTRAINT FK_F467E4118E44C1EF FOREIGN KEY (listid) REFERENCES phplist_list (id) ON DELETE CASCADE'); $this->addSql('CREATE INDEX phplist_listuser_userlistenteredidx ON phplist_listuser (userid, entered, listid)'); $this->addSql('ALTER TABLE phplist_listuser RENAME INDEX userenteredidx TO phplist_listuser_userenteredidx'); - $this->addSql('ALTER TABLE phplist_listuser RENAME INDEX useridx TO phplist_listuser_useridx'); - $this->addSql('ALTER TABLE phplist_listuser RENAME INDEX listidx TO phplist_listuser_listidx'); - $this->addSql('ALTER TABLE phplist_message CHANGE footer footer LONGTEXT DEFAULT NULL, CHANGE modified modified DATETIME NOT NULL, CHANGE userselection userselection LONGTEXT DEFAULT NULL, CHANGE htmlformatted htmlformatted TINYINT(1) NOT NULL, CHANGE astext astext TINYINT(1) NOT NULL, CHANGE ashtml ashtml TINYINT(1) NOT NULL, CHANGE astextandhtml astextandhtml TINYINT(1) NOT NULL, CHANGE aspdf aspdf TINYINT(1) NOT NULL, CHANGE astextandpdf astextandpdf TINYINT(1) NOT NULL, CHANGE viewed viewed INT DEFAULT 0 NOT NULL, CHANGE bouncecount bouncecount INT DEFAULT 0 NOT NULL'); + $this->renameOrCreateIndex($schema, 'phplist_listuser', ['useridx'], 'phplist_listuser_useridx', ['userid']); + $this->renameOrCreateIndex($schema, 'phplist_listuser', ['listidx'], 'phplist_listuser_listidx', ['listid']); + $this->addSql('ALTER TABLE phplist_message CHANGE footer footer LONGTEXT DEFAULT NULL, CHANGE modified modified DATETIME NOT NULL, CHANGE userselection userselection LONGTEXT DEFAULT NULL, CHANGE htmlformatted htmlformatted TINYINT(1) NOT NULL, CHANGE astext astext INT DEFAULT 0 NOT NULL, CHANGE ashtml ashtml INT DEFAULT 0 NOT NULL, CHANGE astextandhtml astextandhtml INT DEFAULT 0 NOT NULL, CHANGE aspdf aspdf INT DEFAULT 0 NOT NULL, CHANGE astextandpdf astextandpdf INT DEFAULT 0 NOT NULL, CHANGE viewed viewed INT DEFAULT 0 NOT NULL, CHANGE bouncecount bouncecount INT DEFAULT 0 NOT NULL'); + $this->addSql('UPDATE phplist_message t LEFT JOIN phplist_admin p ON t.owner = p.id SET t.owner = NULL WHERE t.owner IS NOT NULL AND p.id IS NULL'); + $this->addSql('UPDATE phplist_message t LEFT JOIN phplist_template p ON t.template = p.id SET t.template = NULL WHERE t.template IS NOT NULL AND p.id IS NULL'); $this->addSql('ALTER TABLE phplist_message ADD CONSTRAINT FK_C5D81FCDCF60E67C FOREIGN KEY (owner) REFERENCES phplist_admin (id)'); $this->addSql('ALTER TABLE phplist_message ADD CONSTRAINT FK_C5D81FCD97601F83 FOREIGN KEY (template) REFERENCES phplist_template (id) ON DELETE SET NULL'); $this->addSql('CREATE INDEX IDX_C5D81FCDCF60E67C ON phplist_message (owner)'); @@ -102,11 +114,13 @@ public function up(Schema $schema): void $this->addSql('ALTER TABLE phplist_messagedata CHANGE data data LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL'); $this->addSql('ALTER TABLE phplist_sendprocess CHANGE modified modified DATETIME NOT NULL'); $this->addSql('ALTER TABLE phplist_subscribepage CHANGE active active TINYINT(1) DEFAULT 0 NOT NULL'); + $this->addSql('UPDATE phplist_subscribepage t LEFT JOIN phplist_admin p ON t.owner = p.id SET t.owner = NULL WHERE t.owner IS NOT NULL AND p.id IS NULL'); $this->addSql('ALTER TABLE phplist_subscribepage ADD CONSTRAINT FK_5BAC7737CF60E67C FOREIGN KEY (owner) REFERENCES phplist_admin (id)'); $this->addSql('CREATE INDEX IDX_5BAC7737CF60E67C ON phplist_subscribepage (owner)'); $this->addSql('ALTER TABLE phplist_subscribepage_data CHANGE data data LONGTEXT DEFAULT NULL'); $this->addSql('ALTER TABLE phplist_template RENAME INDEX title TO phplist_template_title'); $this->addSql('ALTER TABLE phplist_templateimage CHANGE template template INT NOT NULL'); + $this->addSql('DELETE t FROM phplist_templateimage t LEFT JOIN phplist_template p ON t.template = p.id WHERE p.id IS NULL'); $this->addSql('ALTER TABLE phplist_templateimage ADD CONSTRAINT FK_30A85BA97601F83 FOREIGN KEY (template) REFERENCES phplist_template (id)'); $this->addSql('ALTER TABLE phplist_templateimage RENAME INDEX templateidx TO phplist_templateimage_templateidx'); $this->addSql('ALTER TABLE phplist_urlcache RENAME INDEX urlindex TO phplist_urlcache_urlindex'); @@ -117,6 +131,7 @@ public function up(Schema $schema): void $this->addSql('ALTER TABLE phplist_user_blacklist RENAME INDEX emailidx TO phplist_user_blacklist_emailidx'); $this->addSql('DROP INDEX email ON phplist_user_blacklist_data'); $this->addSql('ALTER TABLE phplist_user_blacklist_data CHANGE email email VARCHAR(255) NOT NULL, CHANGE data data LONGTEXT DEFAULT NULL, ADD PRIMARY KEY (email)'); + $this->addSql('DELETE t FROM phplist_user_blacklist_data t LEFT JOIN phplist_user_blacklist p ON t.email = p.email WHERE p.email IS NULL'); $this->addSql('ALTER TABLE phplist_user_blacklist_data ADD CONSTRAINT FK_6D67150CE7927C74 FOREIGN KEY (email) REFERENCES phplist_user_blacklist (email) ON DELETE CASCADE'); $this->addSql('ALTER TABLE phplist_user_blacklist_data RENAME INDEX emailidx TO phplist_user_blacklist_data_emailidx'); $this->addSql('ALTER TABLE phplist_user_blacklist_data RENAME INDEX emailnameidx TO phplist_user_blacklist_data_emailnameidx'); @@ -131,31 +146,42 @@ public function up(Schema $schema): void $this->addSql('ALTER TABLE phplist_user_message_view RENAME INDEX useridx TO phplist_user_message_view_useridx'); $this->addSql('ALTER TABLE phplist_user_message_view RENAME INDEX usermsgidx TO phplist_user_message_view_usermsgidx'); $this->addSql('ALTER TABLE phplist_user_user CHANGE confirmed confirmed TINYINT(1) NOT NULL, CHANGE blacklisted blacklisted TINYINT(1) NOT NULL, CHANGE optedin optedin TINYINT(1) NOT NULL, CHANGE bouncecount bouncecount INT NOT NULL, CHANGE modified modified DATETIME NOT NULL, CHANGE uuid uuid VARCHAR(36) NOT NULL, CHANGE htmlemail htmlemail TINYINT(1) NOT NULL, CHANGE passwordchanged passwordchanged DATETIME DEFAULT NULL, CHANGE disabled disabled TINYINT(1) NOT NULL, CHANGE extradata extradata LONGTEXT DEFAULT NULL'); - $this->addSql('ALTER TABLE phplist_user_user RENAME INDEX idxuniqid TO phplist_user_user_idxuniqid'); + $this->renameOrCreateIndex( + $schema, + 'phplist_user_user', + ['idxuniqid', 'idx_phplist_user_user_uniqid'], + 'phplist_user_user_idxuniqid', + ['uniqid'] + ); $this->addSql('ALTER TABLE phplist_user_user RENAME INDEX enteredindex TO phplist_user_user_enteredindex'); - $this->addSql('ALTER TABLE phplist_user_user RENAME INDEX confidx TO phplist_user_user_confidx'); - $this->addSql('ALTER TABLE phplist_user_user RENAME INDEX blidx TO phplist_user_user_blidx'); - $this->addSql('ALTER TABLE phplist_user_user RENAME INDEX optidx TO phplist_user_user_optidx'); + $this->renameOrCreateIndex($schema, 'phplist_user_user', ['confidx'], 'phplist_user_user_confidx', ['confirmed']); + $this->renameOrCreateIndex($schema, 'phplist_user_user', ['blidx'], 'phplist_user_user_blidx', ['blacklisted']); + $this->renameOrCreateIndex($schema, 'phplist_user_user', ['optidx'], 'phplist_user_user_optidx', ['optedin']); $this->addSql('ALTER TABLE phplist_user_user RENAME INDEX uuididx TO phplist_user_user_uuididx'); $this->addSql('ALTER TABLE phplist_user_user RENAME INDEX foreignkey TO phplist_user_user_foreignkey'); $this->addSql('ALTER TABLE phplist_user_user RENAME INDEX email TO phplist_user_user_email'); $this->addSql('ALTER TABLE phplist_user_user_attribute CHANGE value value LONGTEXT DEFAULT NULL'); + $this->addSql('DELETE t FROM phplist_user_user_attribute t LEFT JOIN phplist_user_attribute p ON t.attributeid = p.id WHERE p.id IS NULL'); + $this->addSql('DELETE t FROM phplist_user_user_attribute t LEFT JOIN phplist_user_user p ON t.userid = p.id WHERE p.id IS NULL'); $this->addSql('ALTER TABLE phplist_user_user_attribute ADD CONSTRAINT FK_E24E310878C45AB5 FOREIGN KEY (attributeid) REFERENCES phplist_user_attribute (id) ON DELETE CASCADE'); $this->addSql('ALTER TABLE phplist_user_user_attribute ADD CONSTRAINT FK_E24E3108F132696E FOREIGN KEY (userid) REFERENCES phplist_user_user (id) ON DELETE CASCADE'); $this->addSql('ALTER TABLE phplist_user_user_attribute RENAME INDEX attindex TO phplist_user_user_attribute_attindex'); $this->addSql('ALTER TABLE phplist_user_user_attribute RENAME INDEX attuserid TO phplist_user_user_attribute_attuserid'); $this->addSql('ALTER TABLE phplist_user_user_attribute RENAME INDEX userindex TO phplist_user_user_attribute_userindex'); $this->addSql('ALTER TABLE phplist_user_user_history CHANGE detail detail LONGTEXT DEFAULT NULL, CHANGE systeminfo systeminfo LONGTEXT DEFAULT NULL'); + $this->addSql('DELETE t FROM phplist_user_user_history t LEFT JOIN phplist_user_user p ON t.userid = p.id WHERE p.id IS NULL'); $this->addSql('ALTER TABLE phplist_user_user_history ADD CONSTRAINT FK_6DBB605CF132696E FOREIGN KEY (userid) REFERENCES phplist_user_user (id) ON DELETE CASCADE'); $this->addSql('ALTER TABLE phplist_user_user_history RENAME INDEX dateidx TO phplist_user_user_history_dateidx'); $this->addSql('ALTER TABLE phplist_user_user_history RENAME INDEX userididx TO phplist_user_user_history_userididx'); + $this->addSql('DELETE t FROM phplist_usermessage t LEFT JOIN phplist_user_user p ON t.userid = p.id WHERE p.id IS NULL'); + $this->addSql('DELETE t FROM phplist_usermessage t LEFT JOIN phplist_message p ON t.messageid = p.id WHERE p.id IS NULL'); $this->addSql('ALTER TABLE phplist_usermessage ADD CONSTRAINT FK_7F30F469F132696E FOREIGN KEY (userid) REFERENCES phplist_user_user (id) ON DELETE CASCADE'); $this->addSql('ALTER TABLE phplist_usermessage ADD CONSTRAINT FK_7F30F46931478478 FOREIGN KEY (messageid) REFERENCES phplist_message (id) ON DELETE CASCADE'); $this->addSql('ALTER TABLE phplist_usermessage RENAME INDEX enteredindex TO phplist_usermessage_enteredindex'); $this->addSql('ALTER TABLE phplist_usermessage RENAME INDEX messageidindex TO phplist_usermessage_messageidindex'); - $this->addSql('ALTER TABLE phplist_usermessage RENAME INDEX statusidx TO phplist_usermessage_statusidx'); + $this->renameOrCreateIndex($schema, 'phplist_usermessage', ['statusidx'], 'phplist_usermessage_statusidx', ['status']); $this->addSql('ALTER TABLE phplist_usermessage RENAME INDEX useridindex TO phplist_usermessage_useridindex'); - $this->addSql('ALTER TABLE phplist_usermessage RENAME INDEX viewedidx TO phplist_usermessage_viewedidx'); + $this->renameOrCreateIndex($schema, 'phplist_usermessage', ['viewedidx'], 'phplist_usermessage_viewedidx', ['viewed']); $this->addSql('ALTER TABLE phplist_userstats RENAME INDEX dateindex TO phplist_userstats_dateindex'); $this->addSql('ALTER TABLE phplist_userstats RENAME INDEX itemindex TO phplist_userstats_itemindex'); $this->addSql('ALTER TABLE phplist_userstats RENAME INDEX listdateindex TO phplist_userstats_listdateindex'); diff --git a/src/Migrations/Version20251031072945PostGreInit.php b/src/Migrations/Version20251031072945PostGreInit.php index 80c27956..08076e8d 100644 --- a/src/Migrations/Version20251031072945PostGreInit.php +++ b/src/Migrations/Version20251031072945PostGreInit.php @@ -5,7 +5,6 @@ namespace PhpList\Core\Migrations; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; -use Doctrine\Migrations\AbstractMigration; use Doctrine\DBAL\Schema\Schema; /** @@ -15,7 +14,7 @@ * * Ex: phplist_linktrack_forward phplist_linktrack_forward_urlindex (but there are more) */ -final class Version20251031072945PostGreInit extends AbstractMigration +final class Version20251031072945PostGreInit extends AbstractPrefixedMigration { public function getDescription(): string { @@ -121,7 +120,7 @@ public function up(Schema $schema): void $this->addSql('CREATE INDEX phplist_listuser_userlistenteredidx ON phplist_listuser (userid, entered, listid)'); $this->addSql('CREATE INDEX phplist_listuser_useridx ON phplist_listuser (userid)'); $this->addSql('CREATE INDEX phplist_listuser_listidx ON phplist_listuser (listid)'); - $this->addSql('CREATE TABLE phplist_message (id INT NOT NULL, owner INT DEFAULT NULL, template INT DEFAULT NULL, modified TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, uuid VARCHAR(36) DEFAULT \'\', htmlformatted BOOLEAN NOT NULL, sendformat VARCHAR(20) DEFAULT NULL, astext BOOLEAN NOT NULL, ashtml BOOLEAN NOT NULL, aspdf BOOLEAN NOT NULL, astextandhtml BOOLEAN NOT NULL, astextandpdf BOOLEAN NOT NULL, repeatinterval INT DEFAULT 0, repeatuntil TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, requeueinterval INT DEFAULT 0, requeueuntil TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, embargo TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, status VARCHAR(255) DEFAULT NULL, viewed INT DEFAULT 0 NOT NULL, bouncecount INT DEFAULT 0 NOT NULL, entered TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, sent TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, sendstart TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, subject VARCHAR(255) DEFAULT \'(no subject)\' NOT NULL, message TEXT DEFAULT NULL, textmessage TEXT DEFAULT NULL, footer TEXT DEFAULT NULL, fromfield VARCHAR(255) DEFAULT \'\' NOT NULL, tofield VARCHAR(255) DEFAULT \'\' NOT NULL, replyto VARCHAR(255) DEFAULT \'\' NOT NULL, userselection TEXT DEFAULT NULL, rsstemplate VARCHAR(100) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE phplist_message (id INT NOT NULL, owner INT DEFAULT NULL, template INT DEFAULT NULL, modified TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, uuid VARCHAR(36) DEFAULT \'\', htmlformatted BOOLEAN NOT NULL, sendformat VARCHAR(20) DEFAULT NULL, astext INT DEFAULT 0 NOT NULL, ashtml INT DEFAULT 0 NOT NULL, aspdf INT DEFAULT 0 NOT NULL, astextandhtml INT DEFAULT 0 NOT NULL, astextandpdf INT DEFAULT 0 NOT NULL, repeatinterval INT DEFAULT 0, repeatuntil TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, requeueinterval INT DEFAULT 0, requeueuntil TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, embargo TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, status VARCHAR(255) DEFAULT NULL, viewed INT DEFAULT 0 NOT NULL, bouncecount INT DEFAULT 0 NOT NULL, entered TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, sent TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, sendstart TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, subject VARCHAR(255) DEFAULT \'(no subject)\' NOT NULL, message TEXT DEFAULT NULL, textmessage TEXT DEFAULT NULL, footer TEXT DEFAULT NULL, fromfield VARCHAR(255) DEFAULT \'\' NOT NULL, tofield VARCHAR(255) DEFAULT \'\' NOT NULL, replyto VARCHAR(255) DEFAULT \'\' NOT NULL, userselection TEXT DEFAULT NULL, rsstemplate VARCHAR(100) DEFAULT NULL, PRIMARY KEY(id))'); $this->addSql('CREATE INDEX IDX_C5D81FCDCF60E67C ON phplist_message (owner)'); $this->addSql('CREATE INDEX IDX_C5D81FCD97601F83 ON phplist_message (template)'); $this->addSql('CREATE INDEX phplist_message_uuididx ON phplist_message (uuid)'); diff --git a/src/Migrations/Version20260204094237.php b/src/Migrations/Version20260204094237.php deleted file mode 100644 index 00e7fd91..00000000 --- a/src/Migrations/Version20260204094237.php +++ /dev/null @@ -1,55 +0,0 @@ -connection->getDatabasePlatform(); - $this->skipIf(!$platform instanceof PostgreSQLPlatform, sprintf( - 'Unsupported platform for this migration: %s', - get_class($platform) - )); - - $this->addSql('ALTER TABLE phplist_message ALTER astext TYPE INT USING astext::integer'); - $this->addSql('ALTER TABLE phplist_message ALTER ashtml TYPE INT USING ashtml::integer'); - $this->addSql('ALTER TABLE phplist_message ALTER aspdf TYPE INT USING aspdf::integer'); - $this->addSql('ALTER TABLE phplist_message ALTER astextandhtml TYPE INT USING astextandhtml::integer'); - $this->addSql('ALTER TABLE phplist_message ALTER astextandpdf TYPE INT USING astextandpdf::integer'); - } - - public function down(Schema $schema): void - { - $platform = $this->connection->getDatabasePlatform(); - $this->skipIf(!$platform instanceof PostgreSQLPlatform, sprintf( - 'Unsupported platform for this migration: %s', - get_class($platform) - )); - - $this->addSql('ALTER TABLE phplist_message ALTER astext TYPE BOOLEAN USING (astext::integer <> 0)'); - $this->addSql('ALTER TABLE phplist_message ALTER ashtml TYPE BOOLEAN USING (ashtml::integer <> 0)'); - $this->addSql('ALTER TABLE phplist_message ALTER aspdf TYPE BOOLEAN USING (aspdf::integer <> 0)'); - $this->addSql('ALTER TABLE phplist_message ALTER astextandhtml TYPE BOOLEAN USING (astextandhtml::integer <> 0)'); - $this->addSql('ALTER TABLE phplist_message ALTER astextandpdf TYPE BOOLEAN USING (astextandpdf::integer <> 0)'); - } -} diff --git a/src/Migrations/Version20260820120000MySqlAddMessageStatusIndex.php b/src/Migrations/Version20260820120000MySqlAddMessageStatusIndex.php new file mode 100644 index 00000000..7b2a85df --- /dev/null +++ b/src/Migrations/Version20260820120000MySqlAddMessageStatusIndex.php @@ -0,0 +1,38 @@ +connection->getDatabasePlatform(); + $this->skipIf(!$platform instanceof MySQLPlatform, sprintf( + 'Unsupported platform for this migration: %s', + get_class($platform) + )); + + $this->addSql('CREATE INDEX phplist_message_statusidx ON phplist_message (status, id)'); + } + + public function down(Schema $schema): void + { + $platform = $this->connection->getDatabasePlatform(); + $this->skipIf(!$platform instanceof MySQLPlatform, sprintf( + 'Unsupported platform for this migration: %s', + get_class($platform) + )); + + $this->addSql('DROP INDEX phplist_message_statusidx ON phplist_message'); + } +} diff --git a/src/Migrations/Version20260820120001PostGreAddMessageStatusIndex.php b/src/Migrations/Version20260820120001PostGreAddMessageStatusIndex.php new file mode 100644 index 00000000..2dfe290a --- /dev/null +++ b/src/Migrations/Version20260820120001PostGreAddMessageStatusIndex.php @@ -0,0 +1,38 @@ +connection->getDatabasePlatform(); + $this->skipIf(!$platform instanceof PostgreSQLPlatform, sprintf( + 'Unsupported platform for this migration: %s', + get_class($platform) + )); + + $this->addSql('CREATE INDEX phplist_message_statusidx ON phplist_message (status, id)'); + } + + public function down(Schema $schema): void + { + $platform = $this->connection->getDatabasePlatform(); + $this->skipIf(!$platform instanceof PostgreSQLPlatform, sprintf( + 'Unsupported platform for this migration: %s', + get_class($platform) + )); + + $this->addSql('DROP INDEX phplist_message_statusidx'); + } +} diff --git a/src/Migrations/Version20260827065637.php b/src/Migrations/Version20260827065637.php new file mode 100644 index 00000000..81a20915 --- /dev/null +++ b/src/Migrations/Version20260827065637.php @@ -0,0 +1,108 @@ +connection->getDatabasePlatform(); + $this->skipIf(!$platform instanceof MySQLPlatform, sprintf( + 'Unsupported platform for this migration: %s', + get_class($platform) + )); + + $this->addSql('DROP INDEX loginname ON phplist_admin'); + $this->addSql('DROP INDEX phplist_admin_loginnameidx ON phplist_admin'); + $this->addSql('DELETE FROM phplist_admin WHERE loginname IS NULL'); + $this->addSql('ALTER TABLE phplist_admin CHANGE loginname loginname VARCHAR(66) NOT NULL, CHANGE email email VARCHAR(255) NOT NULL, CHANGE modifiedby modifiedby VARCHAR(66) DEFAULT NULL'); + $this->addSql('CREATE UNIQUE INDEX phplist_admin_loginnameidx ON phplist_admin (loginname)'); + $this->addSql('DELETE FROM phplist_adminattribute WHERE name IS NULL'); + $this->addSql('ALTER TABLE phplist_adminattribute CHANGE name name VARCHAR(255) NOT NULL'); + $this->addSql('DELETE FROM phplist_admintoken WHERE adminid IS NULL'); + $this->addSql('ALTER TABLE phplist_admintoken CHANGE adminid adminid INT NOT NULL'); + $this->addSql('ALTER TABLE phplist_config CHANGE item item VARCHAR(35) NOT NULL'); + $this->addSql('DROP INDEX messageid ON phplist_linktrack'); + $this->addSql('DROP INDEX phplist_linktrack_miduidurlindex ON phplist_linktrack'); + $this->addSql('ALTER TABLE phplist_linktrack CHANGE forward forward VARCHAR(255) DEFAULT NULL'); + $this->addSql('CREATE INDEX phplist_linktrack_latestclickindex ON phplist_linktrack (latestclick)'); + $this->addSql('CREATE UNIQUE INDEX phplist_linktrack_miduidurlindex ON phplist_linktrack (messageid, userid, url)'); +// $this->addSql('UPDATE phplist_list SET name = COALESCE(name, \'\')'); + $this->addSql('ALTER TABLE phplist_list CHANGE name name VARCHAR(255) NOT NULL, CHANGE description description VARCHAR(255) DEFAULT NULL, CHANGE category category VARCHAR(255) DEFAULT NULL'); + $this->addSql('DROP INDEX phplist_message_statusidx ON phplist_message'); + $this->addSql('UPDATE phplist_message SET subject = COALESCE(subject, \'(no subject)\'), fromfield = COALESCE(fromfield, \'\'), tofield = COALESCE(tofield, \'\'), replyto = COALESCE(replyto, \'\'), processed = COALESCE(processed, 0), astext = COALESCE(astext, 0), ashtml = COALESCE(ashtml, 0), astextandhtml = COALESCE(astextandhtml, 0), aspdf = COALESCE(aspdf, 0), astextandpdf = COALESCE(astextandpdf, 0)'); + $this->addSql('ALTER TABLE phplist_message CHANGE subject subject VARCHAR(255) DEFAULT \'(no subject)\' NOT NULL, CHANGE fromfield fromfield VARCHAR(255) DEFAULT \'\' NOT NULL, CHANGE tofield tofield VARCHAR(255) DEFAULT \'\' NOT NULL, CHANGE replyto replyto VARCHAR(255) DEFAULT \'\' NOT NULL, CHANGE message message LONGTEXT DEFAULT NULL, CHANGE textmessage textmessage LONGTEXT DEFAULT NULL, CHANGE processed processed INT UNSIGNED DEFAULT 0 NOT NULL, CHANGE astext astext INT NOT NULL, CHANGE ashtml ashtml INT NOT NULL, CHANGE astextandhtml astextandhtml INT NOT NULL, CHANGE aspdf aspdf INT NOT NULL, CHANGE astextandpdf astextandpdf INT NOT NULL'); + $this->addSql('CREATE INDEX phplist_message_sentidx ON phplist_message (sent)'); + $this->addSql('ALTER TABLE phplist_messagedata CHANGE name name VARCHAR(100) NOT NULL'); + $this->addSql('UPDATE phplist_subscribepage SET title = COALESCE(title, \'\')'); + $this->addSql('ALTER TABLE phplist_subscribepage CHANGE title title VARCHAR(255) NOT NULL'); + $this->addSql('ALTER TABLE phplist_subscribepage_data CHANGE name name VARCHAR(100) NOT NULL'); + $this->addSql('UPDATE phplist_template SET title = COALESCE(title, \'\')'); + $this->addSql('ALTER TABLE phplist_template CHANGE title title VARCHAR(255) NOT NULL'); +// $this->addSql('UPDATE phplist_user_attribute SET name = COALESCE(name, \'\')'); + $this->addSql('ALTER TABLE phplist_user_attribute CHANGE name name VARCHAR(255) NOT NULL'); + $this->addSql('DROP INDEX email_2 ON phplist_user_blacklist_data'); +// $this->addSql('UPDATE phplist_user_blacklist_data SET name = LEFT(COALESCE(name, \'\'), 25)'); + $this->addSql('ALTER TABLE phplist_user_blacklist_data CHANGE name name VARCHAR(25) NOT NULL'); + $this->addSql('DROP INDEX message_lookup ON phplist_user_message_bounce'); + $this->addSql('DROP INDEX emailidx ON phplist_user_user'); +// $this->addSql('UPDATE phplist_user_user SET email = COALESCE(email, \'\'), uniqid = COALESCE(uniqid, \'\')'); + $this->addSql('ALTER TABLE phplist_user_user CHANGE email email VARCHAR(255) NOT NULL, CHANGE uniqid uniqid VARCHAR(255) NOT NULL'); + $this->addSql('DROP INDEX userattid ON phplist_user_user_attribute'); + } + + public function down(Schema $schema): void + { + $platform = $this->connection->getDatabasePlatform(); + $this->skipIf(!$platform instanceof MySQLPlatform, sprintf( + 'Unsupported platform for this migration: %s', + get_class($platform) + )); + + $this->addSql('DROP INDEX phplist_admin_loginnameidx ON phplist_admin'); + $this->addSql('ALTER TABLE phplist_admin CHANGE loginname loginname VARCHAR(66) DEFAULT \'\', CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE modifiedby modifiedby VARCHAR(66) DEFAULT \'\''); + $this->addSql('CREATE UNIQUE INDEX loginname ON phplist_admin (loginname)'); + $this->addSql('CREATE INDEX phplist_admin_loginnameidx ON phplist_admin (loginname)'); + $this->addSql('ALTER TABLE phplist_adminattribute CHANGE name name VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE phplist_admintoken CHANGE adminid adminid INT DEFAULT NULL'); + $this->addSql('ALTER TABLE phplist_config CHANGE item item VARCHAR(35) DEFAULT \'\' NOT NULL'); + $this->addSql('DROP INDEX phplist_linktrack_latestclickindex ON phplist_linktrack'); + $this->addSql('DROP INDEX phplist_linktrack_miduidurlindex ON phplist_linktrack'); + $this->addSql('ALTER TABLE phplist_linktrack CHANGE forward forward TEXT DEFAULT NULL'); + $this->addSql('CREATE UNIQUE INDEX messageid ON phplist_linktrack (messageid, userid, url)'); + $this->addSql('CREATE INDEX phplist_linktrack_miduidurlindex ON phplist_linktrack (messageid, userid, url)'); + $this->addSql('ALTER TABLE phplist_list CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE description description VARCHAR(255) NOT NULL, CHANGE category category VARCHAR(255) NOT NULL'); + $this->addSql('DROP INDEX phplist_message_sentidx ON phplist_message'); + $this->addSql('ALTER TABLE phplist_message CHANGE astext astext INT DEFAULT 0 NOT NULL, CHANGE ashtml ashtml INT DEFAULT 0 NOT NULL, CHANGE aspdf aspdf INT DEFAULT 0 NOT NULL, CHANGE astextandhtml astextandhtml INT DEFAULT 0 NOT NULL, CHANGE astextandpdf astextandpdf INT DEFAULT 0 NOT NULL, CHANGE processed processed INT DEFAULT 0, CHANGE subject subject VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT \'(no subject)\' NOT NULL COLLATE `utf8mb4_general_ci`, CHANGE message message LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_general_ci`, CHANGE textmessage textmessage LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_general_ci`, CHANGE fromfield fromfield VARCHAR(255) DEFAULT NULL, CHANGE tofield tofield VARCHAR(255) DEFAULT NULL, CHANGE replyto replyto VARCHAR(255) DEFAULT NULL'); + $this->addSql('CREATE INDEX phplist_message_statusidx ON phplist_message (status, id)'); + $this->addSql('ALTER TABLE phplist_messagedata CHANGE name name VARCHAR(100) DEFAULT \'\' NOT NULL'); + $this->addSql('ALTER TABLE phplist_subscribepage CHANGE title title VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE phplist_subscribepage_data CHANGE name name VARCHAR(100) DEFAULT \'\' NOT NULL'); + $this->addSql('ALTER TABLE phplist_template CHANGE title title VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE phplist_user_attribute CHANGE name name VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE phplist_user_blacklist_data CHANGE name name VARCHAR(100) DEFAULT NULL'); + $this->addSql('CREATE UNIQUE INDEX email_2 ON phplist_user_blacklist_data (email)'); + $this->addSql('CREATE INDEX message_lookup ON phplist_user_message_bounce (message)'); + $this->addSql('ALTER TABLE phplist_user_user CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE uniqid uniqid VARCHAR(255) DEFAULT NULL'); + $this->addSql('CREATE INDEX emailidx ON phplist_user_user (email)'); + $this->addSql('CREATE INDEX userattid ON phplist_user_user_attribute (attributeid, userid)'); + } +} diff --git a/src/Migrations/_template_migration.php.tpl b/src/Migrations/_template_migration.php.tpl index 72561549..cd2cde8f 100644 --- a/src/Migrations/_template_migration.php.tpl +++ b/src/Migrations/_template_migration.php.tpl @@ -6,7 +6,6 @@ namespace ; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Platforms\MySQLPlatform; -use Doctrine\Migrations\AbstractMigration; use Doctrine\DBAL\Schema\Schema; /** @@ -16,7 +15,7 @@ use Doctrine\DBAL\Schema\Schema; * * Ex: phplist_linktrack_forward phplist_linktrack_forward_urlindex (but there are more) */ -final class extends AbstractMigration +final class extends AbstractPrefixedMigration { public function getDescription(): string { diff --git a/src/Security/HashGenerator.php b/src/Security/HashGenerator.php deleted file mode 100644 index a70acaa3..00000000 --- a/src/Security/HashGenerator.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ -class HashGenerator -{ - /** - * @var string - */ - const PASSWORD_HASH_ALGORITHM = 'sha256'; - - /** - * @param string $plainTextPassword - * - * @return string - */ - public function createPasswordHash(string $plainTextPassword): string - { - return hash(static::PASSWORD_HASH_ALGORITHM, $plainTextPassword); - } -} diff --git a/src/TestingSupport/Traits/DatabaseTestTrait.php b/src/TestingSupport/Traits/DatabaseTestTrait.php index f6f5e551..f6ca6b65 100644 --- a/src/TestingSupport/Traits/DatabaseTestTrait.php +++ b/src/TestingSupport/Traits/DatabaseTestTrait.php @@ -4,7 +4,7 @@ namespace PhpList\Core\TestingSupport\Traits; -use Doctrine\DBAL\Platforms\SqlitePlatform; +use Doctrine\DBAL\Platforms\SQLitePlatform; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\ORM\Tools\ToolsException; @@ -84,7 +84,7 @@ protected function loadSchema(): void $schemaTool = new SchemaTool($this->entityManager); $metadata = $this->entityManager->getMetadataFactory()->getAllMetadata(); - if ($this->entityManager->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) { + if ($this->entityManager->getConnection()->getDatabasePlatform() instanceof SQLitePlatform) { $this->runForSqlite($metadata, $schemaTool); } else { $this->runForMySql($metadata, $schemaTool); diff --git a/src/TestingSupport/Traits/ModelTestTrait.php b/src/TestingSupport/Traits/ModelTestTrait.php index 10ccbae8..47f542c8 100644 --- a/src/TestingSupport/Traits/ModelTestTrait.php +++ b/src/TestingSupport/Traits/ModelTestTrait.php @@ -33,7 +33,7 @@ private function setSubjectId(DomainModel $model, int $id): void * @param string $propertyName * @param mixed $value * - * @return void* + * @return void */ private function setSubjectProperty(DomainModel $model, string $propertyName, mixed $value): void { diff --git a/tests/Integration/Domain/Identity/Repository/AdministratorRepositoryTest.php b/tests/Integration/Domain/Identity/Repository/AdministratorRepositoryTest.php index a69a751b..4b36a190 100644 --- a/tests/Integration/Domain/Identity/Repository/AdministratorRepositoryTest.php +++ b/tests/Integration/Domain/Identity/Repository/AdministratorRepositoryTest.php @@ -42,7 +42,7 @@ protected function tearDown(): void public function testFindReadsModelFromDatabase(): void { - /** @var Administrator $actual */ + /** @var ?Administrator $actual */ $actual = $this->repository->findOneBy(['email' => 'john@example.com']); $this->assertNotNull($actual); @@ -66,7 +66,7 @@ public function testFindReadsModelFromDatabase(): void public function testCreationDateOfExistingModelStaysUnchangedOnUpdate(): void { $id = 1; - /** @var Administrator $model */ + /** @var ?Administrator $model */ $model = $this->repository->find($id); $this->assertNotNull($model); $originalCreationDate = $model->getCreatedAt(); @@ -80,7 +80,7 @@ public function testCreationDateOfExistingModelStaysUnchangedOnUpdate(): void public function testModificationDateOfExistingModelGetsUpdatedOnUpdate(): void { $id = 1; - /** @var Administrator $model */ + /** @var ?Administrator $model */ $model = $this->repository->find($id); $this->assertNotNull($model); diff --git a/tests/Integration/Domain/Identity/Repository/AdministratorTokenRepositoryTest.php b/tests/Integration/Domain/Identity/Repository/AdministratorTokenRepositoryTest.php index 015061d0..55d03460 100644 --- a/tests/Integration/Domain/Identity/Repository/AdministratorTokenRepositoryTest.php +++ b/tests/Integration/Domain/Identity/Repository/AdministratorTokenRepositoryTest.php @@ -26,7 +26,7 @@ class AdministratorTokenRepositoryTest extends WebTestCase use DatabaseTestTrait; use SimilarDatesAssertionTrait; - private ?AdministratorTokenRepository $repository; + private AdministratorTokenRepository $repository; protected function setUp(): void { diff --git a/tests/Integration/Security/AuthenticationTest.php b/tests/Integration/Domain/Identity/Service/AuthenticationTest.php similarity index 96% rename from tests/Integration/Security/AuthenticationTest.php rename to tests/Integration/Domain/Identity/Service/AuthenticationTest.php index 8b1d2d0e..55733e1d 100644 --- a/tests/Integration/Security/AuthenticationTest.php +++ b/tests/Integration/Domain/Identity/Service/AuthenticationTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Security; +namespace PhpList\Core\Tests\Integration\Domain\Identity\Service; use Doctrine\ORM\Tools\SchemaTool; use PhpList\Core\Domain\Identity\Model\Administrator; -use PhpList\Core\Security\Authentication; +use PhpList\Core\Domain\Identity\Service\Authentication; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; use PhpList\Core\Tests\Integration\Domain\Identity\Fixtures\AdministratorFixture; use PhpList\Core\Tests\Integration\Domain\Identity\Fixtures\AdministratorTokenWithAdministratorFixture; diff --git a/tests/Integration/Security/HashGeneratorTest.php b/tests/Integration/Domain/Identity/Service/HashGeneratorTest.php similarity index 89% rename from tests/Integration/Security/HashGeneratorTest.php rename to tests/Integration/Domain/Identity/Service/HashGeneratorTest.php index cc0d810e..9ceb9b95 100644 --- a/tests/Integration/Security/HashGeneratorTest.php +++ b/tests/Integration/Domain/Identity/Service/HashGeneratorTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Integration\Security; +namespace PhpList\Core\Tests\Integration\Domain\Identity\Service; use Doctrine\ORM\Tools\SchemaTool; -use PhpList\Core\Security\HashGenerator; +use PhpList\Core\Domain\Identity\Service\HashGenerator; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; diff --git a/tests/Integration/Domain/Messaging/Fixtures/MessageFixture.php b/tests/Integration/Domain/Messaging/Fixtures/MessageFixture.php index 08113100..faba2cef 100644 --- a/tests/Integration/Domain/Messaging/Fixtures/MessageFixture.php +++ b/tests/Integration/Domain/Messaging/Fixtures/MessageFixture.php @@ -12,6 +12,7 @@ use PhpList\Core\Domain\Messaging\Model\Message\MessageContent; use PhpList\Core\Domain\Messaging\Model\Message\MessageFormat; use PhpList\Core\Domain\Messaging\Model\Message\MessageMetadata; +use PhpList\Core\Domain\Messaging\Model\Message\MessageStatus; use PhpList\Core\Domain\Messaging\Model\Message\MessageOptions; use PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule; use PhpList\Core\Domain\Messaging\Model\Template; @@ -61,7 +62,7 @@ public function load(ObjectManager $manager): void embargo: new DateTime($row['embargo']), ); $metadata = new MessageMetadata( - status: $row['status'], + status: MessageStatus::from($row['status']), bounceCount: (int)$row['bouncecount'], entered: new DateTime($row['entered']), sent: new DateTime($row['sent']), diff --git a/tests/Integration/Domain/Messaging/Repository/MessageRepositoryTest.php b/tests/Integration/Domain/Messaging/Repository/MessageRepositoryTest.php index 29793766..7bd83207 100644 --- a/tests/Integration/Domain/Messaging/Repository/MessageRepositoryTest.php +++ b/tests/Integration/Domain/Messaging/Repository/MessageRepositoryTest.php @@ -8,6 +8,7 @@ use Doctrine\ORM\Tools\SchemaTool; use PhpList\Core\Domain\Configuration\Model\OutputFormat; use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Messaging\Model\Filter\MessageFilter; use PhpList\Core\Domain\Messaging\Model\Message; use PhpList\Core\Domain\Messaging\Model\Message\MessageContent; use PhpList\Core\Domain\Messaging\Model\Message\MessageFormat; @@ -131,4 +132,118 @@ public function testMessageTimestampsAreSetOnPersist(): void self::assertSimilarDates($expectedDate, $message->getUpdatedAt()); } + + private function persistMessage( + Message\MessageStatus $status, + string $subject, + ?Administrator $owner = null + ): Message { + $message = new Message( + new MessageFormat(true, OutputFormat::Text->value), + new MessageSchedule(1, null, 3, null, null), + new MessageMetadata($status), + new MessageContent($subject), + new MessageOptions(), + $owner + ); + + $this->entityManager->persist($message); + + return $message; + } + + public function testGetFilteredAfterIdIncludesOwnerlessMessagesForAnyAdmin(): void + { + $admin = (new Administrator())->setLoginName('owner-test-admin'); + $otherAdmin = (new Administrator())->setLoginName('other-admin'); + $this->entityManager->persist($admin); + $this->entityManager->persist($otherAdmin); + + $this->persistMessage(Message\MessageStatus::Sent, 'Legacy unowned campaign'); + $this->persistMessage(Message\MessageStatus::Sent, 'My own campaign', $admin); + $this->persistMessage(Message\MessageStatus::Sent, "Someone else's campaign", $otherAdmin); + $this->entityManager->flush(); + $this->entityManager->clear(); + $admin = $this->entityManager->getRepository(Administrator::class)->find($admin->getId()); + + $filter = (new MessageFilter())->setOwner($admin); + $result = $this->messageRepository->getFilteredAfterId($filter); + + $subjects = array_map( + static fn (Message $message) => $message->getContent()->getSubject(), + $result->getItems() + ); + self::assertContains('Legacy unowned campaign', $subjects); + self::assertContains('My own campaign', $subjects); + self::assertNotContains("Someone else's campaign", $subjects); + } + + public function testGetFilteredAfterIdFiltersBySingleStatus(): void + { + $this->persistMessage(Message\MessageStatus::Draft, 'Draft one'); + $this->persistMessage(Message\MessageStatus::Sent, 'Sent one'); + $this->entityManager->flush(); + $this->entityManager->clear(); + + $filter = (new MessageFilter())->setStatus('draft'); + $result = $this->messageRepository->getFilteredAfterId($filter); + + self::assertCount(1, $result->getItems()); + self::assertSame('Draft one', $result->getItems()[0]->getContent()->getSubject()); + self::assertSame(1, $result->getTotal()); + } + + public function testGetFilteredAfterIdFiltersByMultipleCommaSeparatedStatuses(): void + { + $this->persistMessage(Message\MessageStatus::Draft, 'Draft one'); + $this->persistMessage(Message\MessageStatus::Submitted, 'Submitted one'); + $this->persistMessage(Message\MessageStatus::Sent, 'Sent one'); + $this->entityManager->flush(); + $this->entityManager->clear(); + + $filter = (new MessageFilter())->setStatus('draft,submitted'); + $result = $this->messageRepository->getFilteredAfterId($filter); + + self::assertCount(2, $result->getItems()); + self::assertSame(2, $result->getTotal()); + } + + public function testGetFilteredAfterIdDefaultsToAscendingOrder(): void + { + $first = $this->persistMessage(Message\MessageStatus::Sent, 'First'); + $second = $this->persistMessage(Message\MessageStatus::Sent, 'Second'); + $this->entityManager->flush(); + $this->entityManager->clear(); + + $result = $this->messageRepository->getFilteredAfterId(new MessageFilter()); + + self::assertSame($first->getId(), $result->getItems()[0]->getId()); + self::assertSame($second->getId(), $result->getItems()[1]->getId()); + } + + public function testGetFilteredAfterIdSortsDescendingAndCursorsBackward(): void + { + $first = $this->persistMessage(Message\MessageStatus::Sent, 'First'); + $second = $this->persistMessage(Message\MessageStatus::Sent, 'Second'); + $third = $this->persistMessage(Message\MessageStatus::Sent, 'Third'); + $this->entityManager->flush(); + $this->entityManager->clear(); + + $filter = (new MessageFilter())->setSortOrder('desc')->setLimit(2); + $firstPage = $this->messageRepository->getFilteredAfterId($filter); + + self::assertCount(2, $firstPage->getItems()); + self::assertSame($third->getId(), $firstPage->getItems()[0]->getId()); + self::assertSame($second->getId(), $firstPage->getItems()[1]->getId()); + self::assertSame(3, $firstPage->getTotal()); + + $secondPageFilter = (new MessageFilter()) + ->setSortOrder('desc') + ->setLimit(2) + ->setLastId($firstPage->getItems()[1]->getId()); + $secondPage = $this->messageRepository->getFilteredAfterId($secondPageFilter); + + self::assertCount(1, $secondPage->getItems()); + self::assertSame($first->getId(), $secondPage->getItems()[0]->getId()); + } } diff --git a/tests/Integration/Domain/Subscription/Repository/SubscriberRepositoryTest.php b/tests/Integration/Domain/Subscription/Repository/SubscriberRepositoryTest.php index 2fdff18b..2aa89b25 100644 --- a/tests/Integration/Domain/Subscription/Repository/SubscriberRepositoryTest.php +++ b/tests/Integration/Domain/Subscription/Repository/SubscriberRepositoryTest.php @@ -5,12 +5,10 @@ namespace PhpList\Core\Tests\Integration\Domain\Subscription\Repository; use DateTime; -use Doctrine\Common\Collections\ArrayCollection; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\ORM\Tools\SchemaTool; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Model\Subscription; -use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use PhpList\Core\Domain\Subscription\Repository\SubscriptionRepository; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; @@ -32,7 +30,6 @@ class SubscriberRepositoryTest extends KernelTestCase use SimilarDatesAssertionTrait; private ?SubscriberRepository $subscriberRepository = null; - private ?SubscriberListRepository $subscriberListRepository = null; private ?SubscriptionRepository $subscriptionRepository = null; protected function setUp(): void @@ -41,7 +38,6 @@ protected function setUp(): void $this->loadSchema(); $this->subscriberRepository = self::getContainer()->get(SubscriberRepository::class); - $this->subscriberListRepository = self::getContainer()->get(SubscriberListRepository::class); $this->subscriptionRepository = self::getContainer()->get(SubscriptionRepository::class); } @@ -195,17 +191,15 @@ public function testFindsAssociatedSubscribedLists() $this->loadFixtures([SubscriberFixture::class, SubscriberListFixture::class, SubscriptionFixture::class]); $id = 1; - /** @var Subscriber $model */ + /** @var ?Subscriber $model */ $model = $this->subscriberRepository->findSubscriberWithSubscriptions($id); - $subscriberLists = new ArrayCollection(); + $subscriberListIds = []; foreach ($model->getSubscriptions() as $subscription) { - $subscriberLists->add($subscription->getSubscriberList()); + $subscriberListIds[] = $subscription->getSubscriberList()->getId(); } - $expectedList = $this->subscriberListRepository->find(2); - $unexpectedList = $this->subscriberListRepository->find(1); - self::assertTrue($subscriberLists->contains($expectedList)); - self::assertFalse($subscriberLists->contains($unexpectedList)); + self::assertContains(2, $subscriberListIds); + self::assertNotContains(1, $subscriberListIds); } public function testRemoveAlsoRemovesAssociatedSubscriptions() diff --git a/tests/Integration/Domain/Subscription/Service/SubscriberDeletionServiceTest.php b/tests/Integration/Domain/Subscription/Service/SubscriberDeletionServiceTest.php index 3a7ae27b..41e54a0f 100644 --- a/tests/Integration/Domain/Subscription/Service/SubscriberDeletionServiceTest.php +++ b/tests/Integration/Domain/Subscription/Service/SubscriberDeletionServiceTest.php @@ -115,7 +115,6 @@ public function testDeleteSubscriberWithRelatedDataDoesNotThrowDoctrineError(): try { $this->subscriberDeletionService->deleteLeavingBlacklist($subscriber); $this->entityManager->flush(); - $this->assertTrue(true, 'No exception was thrown'); } catch (Exception $e) { $this->fail('Exception was thrown: ' . $e->getMessage()); } diff --git a/tests/Unit/Core/EnvironmentTest.php b/tests/Unit/Core/EnvironmentTest.php index 7ac06169..2d96438c 100644 --- a/tests/Unit/Core/EnvironmentTest.php +++ b/tests/Unit/Core/EnvironmentTest.php @@ -36,10 +36,8 @@ public function validEnvironmentDataProvider(): array */ public function testValidateEnvironmentForValidEnvironmentPasses(string $environment): void { + $this->expectNotToPerformAssertions(); Environment::validateEnvironment($environment); - - // Adding an assertion to confirm the method executes without throwing an exception. - self::assertTrue(true); } public function testValidateEnvironmentForInvalidEnvironmentThrowsException(): void diff --git a/tests/Unit/Domain/Analytics/Service/AnalyticsServiceTest.php b/tests/Unit/Domain/Analytics/Service/AnalyticsServiceTest.php index a56dd5d8..a7558747 100644 --- a/tests/Unit/Domain/Analytics/Service/AnalyticsServiceTest.php +++ b/tests/Unit/Domain/Analytics/Service/AnalyticsServiceTest.php @@ -4,7 +4,9 @@ namespace PhpList\Core\Tests\Unit\Domain\Analytics\Service; +use DateInterval; use DateTime; +use DateTimeImmutable; use PhpList\Core\Domain\Analytics\Model\LinkTrack; use PhpList\Core\Domain\Analytics\Repository\UserMessageViewRepository; use PhpList\Core\Domain\Analytics\Service\AnalyticsService; @@ -19,7 +21,6 @@ use PhpList\Core\Domain\Messaging\Repository\UserMessageBounceRepository; use PhpList\Core\Domain\Messaging\Repository\UserMessageForwardRepository; use PhpList\Core\Domain\Messaging\Repository\UserMessageRepository; -use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -186,38 +187,13 @@ public function testGetViewOpensStatistics(): void public function testGetTopDomains(): void { - $subscriber1 = $this->createMock(Subscriber::class); - $subscriber1->method('getEmail')->willReturn('user1@example.com'); - - $subscriber2 = $this->createMock(Subscriber::class); - $subscriber2->method('getEmail')->willReturn('user2@example.com'); - - $subscriber3 = $this->createMock(Subscriber::class); - $subscriber3->method('getEmail')->willReturn('user3@example.com'); - - $subscriber4 = $this->createMock(Subscriber::class); - $subscriber4->method('getEmail')->willReturn('user4@example.com'); - - $subscriber5 = $this->createMock(Subscriber::class); - $subscriber5->method('getEmail')->willReturn('user5@example.com'); - - $subscriber6 = $this->createMock(Subscriber::class); - $subscriber6->method('getEmail')->willReturn('user6@example.com'); - - $subscriber7 = $this->createMock(Subscriber::class); - $subscriber7->method('getEmail')->willReturn('user1@test.com'); - - $subscriber8 = $this->createMock(Subscriber::class); - $subscriber8->method('getEmail')->willReturn('user2@test.com'); - - $subscriber9 = $this->createMock(Subscriber::class); - $subscriber9->method('getEmail')->willReturn('user3@another.com'); - $this->subscriberRepository->expects(self::once()) - ->method('findAll') + ->method('getTopDomains') + ->with(50, 1) ->willReturn([ - $subscriber1, $subscriber2, $subscriber3, $subscriber4, $subscriber5, - $subscriber6, $subscriber7, $subscriber8, $subscriber9 + ['domain' => 'example.com', 'subscribers' => 6], + ['domain' => 'test.com', 'subscribers' => 2], + ['domain' => 'another.com', 'subscribers' => 1], ]); $result = $this->subject->getTopDomains(50, 1); @@ -239,46 +215,12 @@ public function testGetTopDomains(): void public function testGetDomainConfirmationStatistics(): void { - $subscriber1 = $this->createMock(Subscriber::class); - $subscriber1->method('getEmail')->willReturn('user1@example.com'); - $subscriber1->method('isConfirmed')->willReturn(true); - $subscriber1->method('isBlacklisted')->willReturn(false); - - $subscriber2 = $this->createMock(Subscriber::class); - $subscriber2->method('getEmail')->willReturn('user2@example.com'); - $subscriber2->method('isConfirmed')->willReturn(true); - $subscriber2->method('isBlacklisted')->willReturn(false); - - $subscriber3 = $this->createMock(Subscriber::class); - $subscriber3->method('getEmail')->willReturn('user3@example.com'); - $subscriber3->method('isConfirmed')->willReturn(false); - $subscriber3->method('isBlacklisted')->willReturn(false); - - $subscriber4 = $this->createMock(Subscriber::class); - $subscriber4->method('getEmail')->willReturn('user4@example.com'); - $subscriber4->method('isConfirmed')->willReturn(false); - $subscriber4->method('isBlacklisted')->willReturn(false); - - $subscriber5 = $this->createMock(Subscriber::class); - $subscriber5->method('getEmail')->willReturn('user5@example.com'); - $subscriber5->method('isConfirmed')->willReturn(false); - $subscriber5->method('isBlacklisted')->willReturn(true); - - $subscriber6 = $this->createMock(Subscriber::class); - $subscriber6->method('getEmail')->willReturn('user1@test.com'); - $subscriber6->method('isConfirmed')->willReturn(true); - $subscriber6->method('isBlacklisted')->willReturn(false); - - $subscriber7 = $this->createMock(Subscriber::class); - $subscriber7->method('getEmail')->willReturn('user2@test.com'); - $subscriber7->method('isConfirmed')->willReturn(false); - $subscriber7->method('isBlacklisted')->willReturn(false); - $this->subscriberRepository->expects(self::once()) - ->method('findAll') + ->method('getDomainConfirmationStatistics') + ->with(50) ->willReturn([ - $subscriber1, $subscriber2, $subscriber3, $subscriber4, - $subscriber5, $subscriber6, $subscriber7 + ['domain' => 'example.com', 'total' => 5, 'confirmed' => 2, 'unconfirmed' => 2, 'blacklisted' => 1], + ['domain' => 'test.com', 'total' => 2, 'confirmed' => 1, 'unconfirmed' => 1, 'blacklisted' => 0], ]); $result = $this->subject->getDomainConfirmationStatistics(); @@ -311,27 +253,20 @@ public function testGetDomainConfirmationStatistics(): void public function testGetTopLocalParts(): void { - $subscriber1 = $this->createMock(Subscriber::class); - $subscriber1->method('getEmail')->willReturn('user1@example.com'); - - $subscriber2 = $this->createMock(Subscriber::class); - $subscriber2->method('getEmail')->willReturn('user2@example.com'); - - $subscriber3 = $this->createMock(Subscriber::class); - $subscriber3->method('getEmail')->willReturn('user1@test.com'); - - $subscriber4 = $this->createMock(Subscriber::class); - $subscriber4->method('getEmail')->willReturn('admin@example.com'); - - $subscriber5 = $this->createMock(Subscriber::class); - $subscriber5->method('getEmail')->willReturn('info@example.com'); - $this->subscriberRepository->expects(self::once()) - ->method('findAll') + ->method('getTopLocalParts') + ->with(25) ->willReturn([ - $subscriber1, $subscriber2, $subscriber3, $subscriber4, $subscriber5 + ['localPart' => 'user1', 'count' => 2], + ['localPart' => 'user2', 'count' => 1], + ['localPart' => 'admin', 'count' => 1], + ['localPart' => 'info', 'count' => 1], ]); + $this->subscriberRepository->expects(self::once()) + ->method('countWithValidEmail') + ->willReturn(5); + $result = $this->subject->getTopLocalParts(); self::assertArrayHasKey('localParts', $result); @@ -376,4 +311,83 @@ public function testGetSummaryStatistics(): void self::assertEquals(2.0, $result['bounce_rate']['value']); self::assertEquals(0.0, $result['bounce_rate']['change_vs_last_month']); } + + public function testGetCampaignPerformance(): void + { + $endDate = new DateTimeImmutable('today 23:59:59'); + $startDate = $endDate->sub(new DateInterval('P29D'))->modify('00:00:00'); + + $someDay = $startDate->add(new DateInterval('P5D'))->format('Y-m-d'); + + $this->userMessageViewManager->expects(self::once()) + ->method('countViewsGroupedByDay') + ->with($startDate, $endDate) + ->willReturn([$someDay => 7]); + + $this->linkTrackManager->expects(self::once()) + ->method('countClicksGroupedByDay') + ->with($startDate, $endDate) + ->willReturn([$someDay => 3]); + + $result = $this->subject->getCampaignPerformance(); + + self::assertCount(30, $result); + + $matching = array_values(array_filter($result, static fn ($row) => $row['date'] === $someDay)); + self::assertCount(1, $matching); + self::assertSame(7, $matching[0]['opens']); + self::assertSame(3, $matching[0]['clicks']); + + $other = array_values(array_filter($result, static fn ($row) => $row['date'] !== $someDay)); + self::assertSame(0, $other[0]['opens']); + self::assertSame(0, $other[0]['clicks']); + } + + public function testGetRecentCampaigns(): void + { + $limit = 5; + $messageId = 42; + + $messageMetadata = $this->createMock(MessageMetadata::class); + $messageMetadata->method('getViews')->willReturn(80); + $messageMetadata->method('getBounceCount')->willReturn(20); + $messageMetadata->method('getSent')->willReturn(new DateTime('2023-02-01 10:00:00')); + $messageMetadata->method('getStatus')->willReturn(null); + + $messageContent = $this->createMock(MessageContent::class); + $messageContent->method('getSubject')->willReturn('Recent Campaign'); + + $message = $this->createMock(Message::class); + $message->method('getId')->willReturn($messageId); + $message->method('getMetadata')->willReturn($messageMetadata); + $message->method('getContent')->willReturn($messageContent); + + $messageResult = new PaginatedResult([$message], 1, 1, $messageId); + + $this->messageRepository->expects(self::once()) + ->method('getFilteredAfterId') + ->with($this->callback(function (MessageFilter $filter) use ($limit): bool { + return $filter->getLastId() === 0 && $filter->getLimit() === $limit; + })) + ->willReturn($messageResult); + + $this->userMessageViewManager->expects(self::once()) + ->method('countViewsByMessageIds') + ->with([$messageId]) + ->willReturn([$messageId => 40]); + + $this->linkTrackManager->expects(self::once()) + ->method('countUniqueClickersByMessageIds') + ->with([$messageId]) + ->willReturn([$messageId => 10]); + + $result = $this->subject->getRecentCampaigns($limit); + + self::assertCount(1, $result); + self::assertSame('Recent Campaign', $result[0]['name']); + self::assertNull($result[0]['status']); + self::assertSame('2023-02-01', $result[0]['date']); + self::assertSame('40%', $result[0]['open_rate']); + self::assertSame('10%', $result[0]['click_rate']); + } } diff --git a/tests/Unit/Domain/Common/PdfGeneratorTest.php b/tests/Unit/Domain/Common/PdfGeneratorTest.php index 78df55c8..549a2c93 100644 --- a/tests/Unit/Domain/Common/PdfGeneratorTest.php +++ b/tests/Unit/Domain/Common/PdfGeneratorTest.php @@ -16,7 +16,6 @@ public function testCreatePdfBytesProducesNonEmptyPdfWithHeaderAndEof(): void $pdfBytes = $generator->createPdfBytes($text); - $this->assertIsString($pdfBytes); $this->assertNotSame('', $pdfBytes); // Must start with a valid PDF header diff --git a/tests/Unit/Domain/Configuration/Service/Manager/EventLogManagerTest.php b/tests/Unit/Domain/Configuration/Service/Manager/EventLogManagerTest.php index f2d4d949..12d778ab 100644 --- a/tests/Unit/Domain/Configuration/Service/Manager/EventLogManagerTest.php +++ b/tests/Unit/Domain/Configuration/Service/Manager/EventLogManagerTest.php @@ -59,8 +59,7 @@ public function testGetWithFiltersDelegatesToRepository(): void ->with( $this->callback(function (EventLogFilter $filter) { // Use getters to validate - return method_exists($filter, 'getPage') - && $filter->getPage() === 'settings' + return $filter->getPage() === 'settings' && $filter->getLastId() === 100 && $filter->getLimit() === 25 && $filter->getDateFrom() instanceof DateTimeImmutable diff --git a/tests/Unit/Domain/Configuration/Service/MessagePlaceholderProcessorTest.php b/tests/Unit/Domain/Configuration/Service/MessagePlaceholderProcessorTest.php index 77cfe0f9..93594a2a 100644 --- a/tests/Unit/Domain/Configuration/Service/MessagePlaceholderProcessorTest.php +++ b/tests/Unit/Domain/Configuration/Service/MessagePlaceholderProcessorTest.php @@ -163,7 +163,7 @@ public function supports(string $key, PlaceholderContext $ctx): bool { return strtoupper($key) === 'SUPPORT'; } - public function resolve(string $key, PlaceholderContext $ctx): ?string + public function resolve(string $key, PlaceholderContext $ctx): string { return 'SVAL'; } diff --git a/tests/Unit/Domain/Configuration/Service/Provider/ConfigProviderTest.php b/tests/Unit/Domain/Configuration/Service/Provider/ConfigProviderTest.php index ab6e90c5..bd7eee08 100644 --- a/tests/Unit/Domain/Configuration/Service/Provider/ConfigProviderTest.php +++ b/tests/Unit/Domain/Configuration/Service/Provider/ConfigProviderTest.php @@ -71,7 +71,7 @@ public function testIsEnabledUsesRepositoryValueWhenPresent(): void $this->repo ->expects($this->once()) ->method('findOneBy') - ->with(['item' => $key->value]) + ->with(['key' => $key->value]) ->willReturn($configEntity); // Defaults should not be consulted if repo has value @@ -90,7 +90,7 @@ public function testIsEnabledFallsBackToDefaultsWhenRepoMissing(): void $this->repo ->expects($this->once()) ->method('findOneBy') - ->with(['item' => $key->value]) + ->with(['key' => $key->value]) ->willReturn(null); $this->defaults diff --git a/tests/Unit/Domain/Identity/Model/AdministratorTest.php b/tests/Unit/Domain/Identity/Model/AdministratorTest.php index cf90e0c9..29190e92 100644 --- a/tests/Unit/Domain/Identity/Model/AdministratorTest.php +++ b/tests/Unit/Domain/Identity/Model/AdministratorTest.php @@ -69,11 +69,6 @@ public function testSetEmailAddressSetsEmailAddress(): void self::assertSame($value, $this->subject->getEmail()); } - public function testGetUpdatedAtInitiallyReturnsNotNull(): void - { - self::assertNotNull($this->subject->getUpdatedAt()); - } - public function testUpdateModificationDateSetsModificationDateToNow(): void { $this->subject->setEmail('update@email.com'); diff --git a/tests/Unit/Domain/Identity/Model/AdministratorTokenTest.php b/tests/Unit/Domain/Identity/Model/AdministratorTokenTest.php index 84a98df7..e2d88726 100644 --- a/tests/Unit/Domain/Identity/Model/AdministratorTokenTest.php +++ b/tests/Unit/Domain/Identity/Model/AdministratorTokenTest.php @@ -89,9 +89,4 @@ public function testGenerateKeyCreatesDifferentKeysForEachCall(): void self::assertNotSame($firstKey, $secondKey); } - - public function testGetAdministratorReturnsConstructorProvidedAdministrator(): void - { - self::assertNotNull($this->subject->getAdministrator()); - } } diff --git a/tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php b/tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php index 863e69f3..2165272c 100644 --- a/tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php +++ b/tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php @@ -5,7 +5,7 @@ namespace PhpList\Core\Tests\Unit\Domain\Identity\Service; use PhpList\Core\Domain\Common\Model\PaginatedResult; -use PhpList\Core\Domain\Identity\Exception\AttributeDefinitionCreationException; +use PhpList\Core\Domain\Common\Exception\AttributeDefinitionCreationException; use PhpList\Core\Domain\Identity\Model\AdminAttributeDefinition; use PhpList\Core\Domain\Identity\Model\Dto\AdminAttributeDefinitionDto; use PhpList\Core\Domain\Identity\Repository\AdminAttributeDefinitionRepository; diff --git a/tests/Unit/Domain/Identity/Service/AdminCopyEmailSenderTest.php b/tests/Unit/Domain/Identity/Service/AdminCopyEmailSenderTest.php index 6f2e4cb4..fdc2e483 100644 --- a/tests/Unit/Domain/Identity/Service/AdminCopyEmailSenderTest.php +++ b/tests/Unit/Domain/Identity/Service/AdminCopyEmailSenderTest.php @@ -78,8 +78,7 @@ public function testSendsToListOwnersWhenFlagEnabled(): void $recipient = $envelope->getRecipients()[0] ?? null; $expectedRecipient = $emails[$invocationIndex++] ?? null; - return $sender !== null - && $sender->getAddress() === $bounce + return $sender->getAddress() === $bounce && $recipient !== null && $recipient->getAddress() === $expectedRecipient; }) @@ -201,7 +200,6 @@ function (Email $email, Envelope $envelope) use (&$sendCalls): void { $senderAddress = $envelope->getSender(); $recipient = $envelope->getRecipients()[0] ?? null; - $this->assertNotNull($senderAddress); $this->assertSame($bounce, $senderAddress->getAddress()); $this->assertNotNull($recipient); diff --git a/tests/Unit/Domain/Identity/Service/AdministratorManagerTest.php b/tests/Unit/Domain/Identity/Service/AdministratorManagerTest.php index 0a56460f..22534b49 100644 --- a/tests/Unit/Domain/Identity/Service/AdministratorManagerTest.php +++ b/tests/Unit/Domain/Identity/Service/AdministratorManagerTest.php @@ -9,7 +9,7 @@ use PhpList\Core\Domain\Identity\Model\Dto\CreateAdministratorDto; use PhpList\Core\Domain\Identity\Model\Dto\UpdateAdministratorDto; use PhpList\Core\Domain\Identity\Service\Manager\AdministratorManager; -use PhpList\Core\Security\HashGenerator; +use PhpList\Core\Domain\Identity\Service\HashGenerator; use PHPUnit\Framework\TestCase; class AdministratorManagerTest extends TestCase @@ -85,7 +85,5 @@ public function testDeleteAdministrator(): void $manager = new AdministratorManager($entityManager, $hashGenerator); $manager->deleteAdministrator($admin); - - $this->assertTrue(true); } } diff --git a/tests/Unit/Security/AuthenticationTest.php b/tests/Unit/Domain/Identity/Service/AuthenticationTest.php similarity index 96% rename from tests/Unit/Security/AuthenticationTest.php rename to tests/Unit/Domain/Identity/Service/AuthenticationTest.php index 58f75f61..79dd3a83 100644 --- a/tests/Unit/Security/AuthenticationTest.php +++ b/tests/Unit/Domain/Identity/Service/AuthenticationTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Security; +namespace PhpList\Core\Tests\Unit\Domain\Identity\Service; use PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Identity\Model\AdministratorToken; -use PhpList\Core\Security\Authentication; +use PhpList\Core\Domain\Identity\Service\Authentication; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; diff --git a/tests/Unit/Domain/Identity/Service/HashGeneratorTest.php b/tests/Unit/Domain/Identity/Service/HashGeneratorTest.php new file mode 100644 index 00000000..480afca3 --- /dev/null +++ b/tests/Unit/Domain/Identity/Service/HashGeneratorTest.php @@ -0,0 +1,84 @@ + + */ +class HashGeneratorTest extends TestCase +{ + private HashGenerator $subject; + + protected function setUp(): void + { + $this->subject = new HashGenerator(); + } + + public function testCreatePasswordHashCreatesPasswordHashCompatibleHash(): void + { + $hash = $this->subject->createPasswordHash('Portal'); + + self::assertNotFalse(password_get_info($hash)['algo']); + } + + public function testCreatePasswordHashCalledTwoTimesWithSamePasswordCreatesDifferentHashes(): void + { + $password = 'Aperture Science'; + + $hash1 = $this->subject->createPasswordHash($password); + $hash2 = $this->subject->createPasswordHash($password); + + self::assertNotSame($hash1, $hash2); + } + + public function testVerifyPasswordForMatchingPasswordAndHashReturnsTrue(): void + { + $password = 'Cave Johnson'; + $hash = $this->subject->createPasswordHash($password); + + self::assertTrue($this->subject->verifyPassword($password, $hash)); + } + + public function testVerifyPasswordForNonMatchingPasswordAndHashReturnsFalse(): void + { + $hash = $this->subject->createPasswordHash('Mel'); + + self::assertFalse($this->subject->verifyPassword('Cave Johnson', $hash)); + } + + public function testVerifyPasswordForMatchingPasswordAndLegacyHashReturnsTrue(): void + { + $password = 'Bazinga!'; + $legacyHash = hash(HashGenerator::LEGACY_PASSWORD_HASH_ALGORITHM, $password); + + self::assertTrue($this->subject->verifyPassword($password, $legacyHash)); + } + + public function testVerifyPasswordForNonMatchingPasswordAndLegacyHashReturnsFalse(): void + { + $legacyHash = hash(HashGenerator::LEGACY_PASSWORD_HASH_ALGORITHM, 'Bazinga!'); + + self::assertFalse($this->subject->verifyPassword('wrong-password', $legacyHash)); + } + + public function testIsLegacyHashForSha256HashReturnsTrue(): void + { + $legacyHash = hash(HashGenerator::LEGACY_PASSWORD_HASH_ALGORITHM, 'Bazinga!'); + + self::assertTrue($this->subject->isLegacyHash($legacyHash)); + } + + public function testIsLegacyHashForPasswordHashHashReturnsFalse(): void + { + $hash = $this->subject->createPasswordHash('Bazinga!'); + + self::assertFalse($this->subject->isLegacyHash($hash)); + } +} diff --git a/tests/Unit/Domain/Identity/Service/PasswordManagerTest.php b/tests/Unit/Domain/Identity/Service/PasswordManagerTest.php index c97547ff..d212ca1e 100644 --- a/tests/Unit/Domain/Identity/Service/PasswordManagerTest.php +++ b/tests/Unit/Domain/Identity/Service/PasswordManagerTest.php @@ -11,7 +11,7 @@ use PhpList\Core\Domain\Identity\Repository\AdminPasswordRequestRepository; use PhpList\Core\Domain\Identity\Service\Manager\PasswordManager; use PhpList\Core\Domain\Messaging\Message\PasswordResetMessage; -use PhpList\Core\Security\HashGenerator; +use PhpList\Core\Domain\Identity\Service\HashGenerator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -96,7 +96,6 @@ public function testGeneratePasswordResetTokenCleansUpExistingRequests(): void $token = $this->subject->generatePasswordResetToken($email); - $this->assertIsString($token); $this->assertNotEmpty($token); } diff --git a/tests/Unit/Bounce/Command/ProcessBouncesCommandTest.php b/tests/Unit/Domain/Messaging/Command/ProcessBouncesCommandTest.php similarity index 95% rename from tests/Unit/Bounce/Command/ProcessBouncesCommandTest.php rename to tests/Unit/Domain/Messaging/Command/ProcessBouncesCommandTest.php index 4ab6d556..130a258f 100644 --- a/tests/Unit/Bounce/Command/ProcessBouncesCommandTest.php +++ b/tests/Unit/Domain/Messaging/Command/ProcessBouncesCommandTest.php @@ -2,16 +2,16 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Command; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Command; use Doctrine\ORM\EntityManagerInterface; use Exception; -use PhpList\Core\Bounce\Command\ProcessBouncesCommand; -use PhpList\Core\Bounce\Service\ConsecutiveBounceHandler; -use PhpList\Core\Bounce\Service\LockService; -use PhpList\Core\Bounce\Service\Processor\AdvancedBounceRulesProcessor; -use PhpList\Core\Bounce\Service\Processor\BounceProtocolProcessor; -use PhpList\Core\Bounce\Service\Processor\UnidentifiedBounceReprocessor; +use PhpList\Core\Domain\Messaging\Command\ProcessBouncesCommand; +use PhpList\Core\Domain\Messaging\Service\ConsecutiveBounceHandler; +use PhpList\Core\Domain\Messaging\Service\LockService; +use PhpList\Core\Domain\Messaging\Service\Processor\AdvancedBounceRulesProcessor; +use PhpList\Core\Domain\Messaging\Service\Processor\BounceProtocolProcessor; +use PhpList\Core\Domain\Messaging\Service\Processor\UnidentifiedBounceReprocessor; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; diff --git a/tests/Unit/Domain/Messaging/Model/SubscriberListTest.php b/tests/Unit/Domain/Messaging/Model/SubscriberListTest.php index 87334cfe..b3eeecaa 100644 --- a/tests/Unit/Domain/Messaging/Model/SubscriberListTest.php +++ b/tests/Unit/Domain/Messaging/Model/SubscriberListTest.php @@ -52,9 +52,9 @@ public function testUpdateCreationDateSetsCreationDateToNow(): void self::assertSimilarDates(new DateTime(), $this->subscriberList->getCreatedAt()); } - public function testgetUpdatedAtInitiallyReturnsNull(): void + public function testGetUpdatedAtInitiallyReturnsCreationTime(): void { - self::assertNull($this->subscriberList->getUpdatedAt()); + self::assertSimilarDates(new DateTime(), $this->subscriberList->getUpdatedAt()); } public function testUpdateModificationDateSetsModificationDateToNow(): void @@ -77,9 +77,9 @@ public function testSetNameSetsName(): void self::assertSame($value, $this->subscriberList->getName()); } - public function testGetDescriptionInitiallyReturnsEmptyString(): void + public function testGetDescriptionInitiallyReturnsNull(): void { - self::assertSame('', $this->subscriberList->getDescription()); + self::assertSame(null, $this->subscriberList->getDescription()); } public function testSetDescriptionSetsDescription(): void @@ -128,9 +128,9 @@ public function testSetPublicSetsPublic(): void self::assertTrue($this->subscriberList->isPublic()); } - public function testGetCategoryInitiallyReturnsEmptyString(): void + public function testGetCategoryInitiallyReturnsNull(): void { - self::assertSame('', $this->subscriberList->getCategory()); + self::assertSame(null, $this->subscriberList->getCategory()); } public function testSetCategorySetsCategory(): void diff --git a/tests/Unit/Bounce/Service/BounceActionResolverTest.php b/tests/Unit/Domain/Messaging/Service/BounceActionResolverTest.php similarity index 91% rename from tests/Unit/Bounce/Service/BounceActionResolverTest.php rename to tests/Unit/Domain/Messaging/Service/BounceActionResolverTest.php index 92b1054d..49d4aadb 100644 --- a/tests/Unit/Bounce/Service/BounceActionResolverTest.php +++ b/tests/Unit/Domain/Messaging/Service/BounceActionResolverTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service; -use PhpList\Core\Bounce\Service\BounceActionResolver; -use PhpList\Core\Bounce\Service\Handler\BounceActionHandlerInterface; +use PhpList\Core\Domain\Messaging\Service\BounceActionResolver; +use PhpList\Core\Domain\Messaging\Service\Handler\BounceActionHandlerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use RuntimeException; diff --git a/tests/Unit/Domain/Messaging/Service/Builder/EmailBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/EmailBuilderTest.php index 90af07cb..f0181e83 100644 --- a/tests/Unit/Domain/Messaging/Service/Builder/EmailBuilderTest.php +++ b/tests/Unit/Domain/Messaging/Service/Builder/EmailBuilderTest.php @@ -176,7 +176,7 @@ public function testBuildsHtmlPreferredWithAttachments(): void $this->templateImageEmbedder ->expects($this->once()) ->method('__invoke') - ->with(html: '

HTML

', messageId: 777) + ->with('

HTML

', 777) ->willReturn('

HTML

'); $this->attachmentAdder ->expects($this->once()) diff --git a/tests/Unit/Domain/Messaging/Service/Builder/ForwardEmailBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/ForwardEmailBuilderTest.php index 8c1c7c49..2ca82e74 100644 --- a/tests/Unit/Domain/Messaging/Service/Builder/ForwardEmailBuilderTest.php +++ b/tests/Unit/Domain/Messaging/Service/Builder/ForwardEmailBuilderTest.php @@ -130,7 +130,7 @@ public function testBuildsForwardEmailWithSubjectPrefixHeadersAndReplyTo(): void $this->templateImageEmbedder ->expects(self::once()) ->method('__invoke') - ->with(html: '

HTML

', messageId: 99) + ->with('

HTML

', 99) ->willReturn('

HTML

'); $this->attachmentAdder diff --git a/tests/Unit/Domain/Messaging/Service/Builder/SystemEmailBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/SystemEmailBuilderTest.php index b003ca32..ddfa6438 100644 --- a/tests/Unit/Domain/Messaging/Service/Builder/SystemEmailBuilderTest.php +++ b/tests/Unit/Domain/Messaging/Service/Builder/SystemEmailBuilderTest.php @@ -145,7 +145,7 @@ public function testBuildsEmailWithExpectedHeadersAndBodiesInDevMode(): void $this->templateImageEmbedder->expects($this->once()) ->method('__invoke') - ->with(html: '

HTML

', messageId: 777) + ->with('

HTML

', 777) ->willReturn('

HTML

'); $builder = $this->makeBuilder( diff --git a/tests/Unit/Bounce/Service/ConsecutiveBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/ConsecutiveBounceHandlerTest.php similarity index 96% rename from tests/Unit/Bounce/Service/ConsecutiveBounceHandlerTest.php rename to tests/Unit/Domain/Messaging/Service/ConsecutiveBounceHandlerTest.php index fbfdfa8a..55825ea9 100644 --- a/tests/Unit/Bounce/Service/ConsecutiveBounceHandlerTest.php +++ b/tests/Unit/Domain/Messaging/Service/ConsecutiveBounceHandlerTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service; -use PhpList\Core\Bounce\Service\ConsecutiveBounceHandler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; -use PhpList\Core\Bounce\Service\SubscriberBlacklistService; +use PhpList\Core\Domain\Messaging\Service\ConsecutiveBounceHandler; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\SubscriberBlacklistService; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; diff --git a/tests/Unit/Domain/Messaging/Service/ForwardContentServiceTest.php b/tests/Unit/Domain/Messaging/Service/ForwardContentServiceTest.php index 8a7bc67a..1ead919a 100644 --- a/tests/Unit/Domain/Messaging/Service/ForwardContentServiceTest.php +++ b/tests/Unit/Domain/Messaging/Service/ForwardContentServiceTest.php @@ -92,9 +92,9 @@ public function testProcessesLinksAndDelegatesToBuilder(): void ->expects(self::once()) ->method('processMessageLinks') ->with( - campaignId: 42, - cachedMessageDto: $cached, - subscriber: $subscriber + 42, + $cached, + $subscriber ) ->willReturn($processed); @@ -103,14 +103,14 @@ public function testProcessesLinksAndDelegatesToBuilder(): void ->expects(self::once()) ->method('buildForwardEmail') ->with( - messageId: 42, - friendEmail: 'f@example.com', - forwardedBy: $subscriber, - data: $processed, - htmlPref: true, - fromName: 'From Name', - fromEmail: 'from@example.com', - forwardedPersonalNote: 'note' + 42, + 'f@example.com', + $subscriber, + $processed, + true, + 'From Name', + 'from@example.com', + 'note' ) ->willReturn([$expectedEmail, OutputFormat::Text]); @@ -127,7 +127,6 @@ public function testProcessesLinksAndDelegatesToBuilder(): void ) ); - self::assertIsArray($result); self::assertSame($expectedEmail, $result[0]); self::assertSame(OutputFormat::Text, $result[1]); } diff --git a/tests/Unit/Domain/Messaging/Service/ForwardDeliveryServiceTest.php b/tests/Unit/Domain/Messaging/Service/ForwardDeliveryServiceTest.php index ca02a0b6..0bd7b238 100644 --- a/tests/Unit/Domain/Messaging/Service/ForwardDeliveryServiceTest.php +++ b/tests/Unit/Domain/Messaging/Service/ForwardDeliveryServiceTest.php @@ -79,10 +79,10 @@ public function testMarkSentDelegatesToManager(): void $this->forwardManager->expects(self::once()) ->method('create') ->with( - subscriber: self::identicalTo($subscriber), - campaign: self::identicalTo($campaign), - friendEmail: $friendEmail, - status: 'sent' + self::identicalTo($subscriber), + self::identicalTo($campaign), + $friendEmail, + 'sent' ); $service->markSent($campaign, $subscriber, $friendEmail); @@ -103,10 +103,10 @@ public function testMarkFailedDelegatesToManager(): void $this->forwardManager->expects(self::once()) ->method('create') ->with( - subscriber: self::identicalTo($subscriber), - campaign: self::identicalTo($campaign), - friendEmail: $friendEmail, - status: 'failed' + self::identicalTo($subscriber), + self::identicalTo($campaign), + $friendEmail, + 'failed' ); $service->markFailed($campaign, $subscriber, $friendEmail); diff --git a/tests/Unit/Domain/Messaging/Service/ForwardingStatsServiceTest.php b/tests/Unit/Domain/Messaging/Service/ForwardingStatsServiceTest.php index cc8f34bc..a84747fd 100644 --- a/tests/Unit/Domain/Messaging/Service/ForwardingStatsServiceTest.php +++ b/tests/Unit/Domain/Messaging/Service/ForwardingStatsServiceTest.php @@ -40,8 +40,6 @@ public function testNoAttributeConfiguredDoesNothing(): void $service->incrementFriendsCount($subscriber); $service->updateFriendsCount($subscriber); - // reached without interactions - self::assertTrue(true); } public function testIncrementThenUpdatePersistsAndResets(): void @@ -63,16 +61,16 @@ public function testIncrementThenUpdatePersistsAndResets(): void $this->valueRepo->expects(self::once()) ->method('findOneBySubscriberAndAttributeName') - ->with(subscriber: self::identicalTo($subscriber), attributeName: 'FriendsForwarded') + ->with(self::identicalTo($subscriber), 'FriendsForwarded') ->willReturn($existing); // After two increments (3 -> 4 -> 5), update should persist '5' $this->attrManager->expects(self::once()) ->method('createOrUpdateByName') ->with( - subscriber: self::identicalTo($subscriber), - attributeName: 'FriendsForwarded', - value: '5' + self::identicalTo($subscriber), + 'FriendsForwarded', + '5' ); $service->incrementFriendsCount($subscriber); @@ -82,7 +80,6 @@ public function testIncrementThenUpdatePersistsAndResets(): void // Second update attempt should be a no-op due to cache reset $this->attrManager->expects(self::never())->method('createOrUpdateByName'); $service->updateFriendsCount($subscriber); - self::assertTrue(true); } public function testCacheIsolationBySubscriber(): void @@ -99,7 +96,7 @@ public function testCacheIsolationBySubscriber(): void // Initial load for A returns 0 $this->valueRepo->expects(self::once()) ->method('findOneBySubscriberAndAttributeName') - ->with(subscriber: self::identicalTo($subscriberA), attributeName: 'FriendsForwarded') + ->with(self::identicalTo($subscriberA), 'FriendsForwarded') ->willReturn(null); // cache for A becomes 1 $service->incrementFriendsCount($subscriberA); @@ -108,9 +105,9 @@ public function testCacheIsolationBySubscriber(): void $this->attrManager->expects(self::once()) ->method('createOrUpdateByName') ->with( - subscriber: self::identicalTo($subscriberA), - attributeName: 'FriendsForwarded', - value: '1' + self::identicalTo($subscriberA), + 'FriendsForwarded', + '1' ); // Calling update for B must be a no-op (cache belongs to A) $service->updateFriendsCount($subscriberB); diff --git a/tests/Unit/Bounce/Service/Handler/BlacklistEmailAndDeleteBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandlerTest.php similarity index 90% rename from tests/Unit/Bounce/Service/Handler/BlacklistEmailAndDeleteBounceHandlerTest.php rename to tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandlerTest.php index c7c2260d..03ec3779 100644 --- a/tests/Unit/Bounce/Service/Handler/BlacklistEmailAndDeleteBounceHandlerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandlerTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service\Handler; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Handler\BlacklistEmailAndDeleteBounceHandler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; -use PhpList\Core\Bounce\Service\SubscriberBlacklistService; +use PhpList\Core\Domain\Messaging\Service\Handler\BlacklistEmailAndDeleteBounceHandler; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\SubscriberBlacklistService; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; diff --git a/tests/Unit/Bounce/Service/Handler/BlacklistEmailHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailHandlerTest.php similarity index 91% rename from tests/Unit/Bounce/Service/Handler/BlacklistEmailHandlerTest.php rename to tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailHandlerTest.php index b5b06e59..c465b10e 100644 --- a/tests/Unit/Bounce/Service/Handler/BlacklistEmailHandlerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailHandlerTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service\Handler; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Handler\BlacklistEmailHandler; -use PhpList\Core\Bounce\Service\SubscriberBlacklistService; +use PhpList\Core\Domain\Messaging\Service\Handler\BlacklistEmailHandler; +use PhpList\Core\Domain\Messaging\Service\SubscriberBlacklistService; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; use PHPUnit\Framework\MockObject\MockObject; diff --git a/tests/Unit/Bounce/Service/Handler/BlacklistUserAndDeleteBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandlerTest.php similarity index 92% rename from tests/Unit/Bounce/Service/Handler/BlacklistUserAndDeleteBounceHandlerTest.php rename to tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandlerTest.php index e2975d37..9de89861 100644 --- a/tests/Unit/Bounce/Service/Handler/BlacklistUserAndDeleteBounceHandlerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandlerTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service\Handler; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Handler\BlacklistUserAndDeleteBounceHandler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; -use PhpList\Core\Bounce\Service\SubscriberBlacklistService; +use PhpList\Core\Domain\Messaging\Service\Handler\BlacklistUserAndDeleteBounceHandler; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\SubscriberBlacklistService; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; diff --git a/tests/Unit/Bounce/Service/Handler/BlacklistUserHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserHandlerTest.php similarity index 92% rename from tests/Unit/Bounce/Service/Handler/BlacklistUserHandlerTest.php rename to tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserHandlerTest.php index 153faa1c..51144868 100644 --- a/tests/Unit/Bounce/Service/Handler/BlacklistUserHandlerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserHandlerTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service\Handler; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Handler\BlacklistUserHandler; -use PhpList\Core\Bounce\Service\SubscriberBlacklistService; +use PhpList\Core\Domain\Messaging\Service\Handler\BlacklistUserHandler; +use PhpList\Core\Domain\Messaging\Service\SubscriberBlacklistService; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; use PHPUnit\Framework\MockObject\MockObject; diff --git a/tests/Unit/Bounce/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandlerTest.php similarity index 94% rename from tests/Unit/Bounce/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandlerTest.php rename to tests/Unit/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandlerTest.php index 9625d348..ce7fdc16 100644 --- a/tests/Unit/Bounce/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandlerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandlerTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service\Handler; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Handler\DecreaseCountConfirmUserAndDeleteBounceHandler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Handler\DecreaseCountConfirmUserAndDeleteBounceHandler; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; diff --git a/tests/Unit/Bounce/Service/Handler/DeleteBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/DeleteBounceHandlerTest.php similarity index 83% rename from tests/Unit/Bounce/Service/Handler/DeleteBounceHandlerTest.php rename to tests/Unit/Domain/Messaging/Service/Handler/DeleteBounceHandlerTest.php index 1455ab83..a87ba785 100644 --- a/tests/Unit/Bounce/Service/Handler/DeleteBounceHandlerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Handler/DeleteBounceHandlerTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service\Handler; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Handler\DeleteBounceHandler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Handler\DeleteBounceHandler; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Messaging\Model\Bounce; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Bounce/Service/Handler/DeleteUserAndBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/DeleteUserAndBounceHandlerTest.php similarity index 91% rename from tests/Unit/Bounce/Service/Handler/DeleteUserAndBounceHandlerTest.php rename to tests/Unit/Domain/Messaging/Service/Handler/DeleteUserAndBounceHandlerTest.php index 768efd0c..f5974fb6 100644 --- a/tests/Unit/Bounce/Service/Handler/DeleteUserAndBounceHandlerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Handler/DeleteUserAndBounceHandlerTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service\Handler; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Handler\DeleteUserAndBounceHandler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Handler\DeleteUserAndBounceHandler; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager; diff --git a/tests/Unit/Bounce/Service/Handler/DeleteUserHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/DeleteUserHandlerTest.php similarity index 94% rename from tests/Unit/Bounce/Service/Handler/DeleteUserHandlerTest.php rename to tests/Unit/Domain/Messaging/Service/Handler/DeleteUserHandlerTest.php index af61b8d5..427f8146 100644 --- a/tests/Unit/Bounce/Service/Handler/DeleteUserHandlerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Handler/DeleteUserHandlerTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service\Handler; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Handler\DeleteUserHandler; +use PhpList\Core\Domain\Messaging\Service\Handler\DeleteUserHandler; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager; use PHPUnit\Framework\MockObject\MockObject; diff --git a/tests/Unit/Bounce/Service/Handler/UnconfirmUserAndDeleteBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandlerTest.php similarity index 93% rename from tests/Unit/Bounce/Service/Handler/UnconfirmUserAndDeleteBounceHandlerTest.php rename to tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandlerTest.php index 92b146fb..f6acbee1 100644 --- a/tests/Unit/Bounce/Service/Handler/UnconfirmUserAndDeleteBounceHandlerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandlerTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service\Handler; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Handler\UnconfirmUserAndDeleteBounceHandler; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Handler\UnconfirmUserAndDeleteBounceHandler; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; diff --git a/tests/Unit/Bounce/Service/Handler/UnconfirmUserHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserHandlerTest.php similarity index 94% rename from tests/Unit/Bounce/Service/Handler/UnconfirmUserHandlerTest.php rename to tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserHandlerTest.php index dcc0c0d8..fbbc265a 100644 --- a/tests/Unit/Bounce/Service/Handler/UnconfirmUserHandlerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserHandlerTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service\Handler; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Handler; -use PhpList\Core\Bounce\Service\Handler\UnconfirmUserHandler; +use PhpList\Core\Domain\Messaging\Service\Handler\UnconfirmUserHandler; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; diff --git a/tests/Unit/Bounce/Service/LockServiceTest.php b/tests/Unit/Domain/Messaging/Service/LockServiceTest.php similarity index 96% rename from tests/Unit/Bounce/Service/LockServiceTest.php rename to tests/Unit/Domain/Messaging/Service/LockServiceTest.php index 8577ef5f..b9cb9c29 100644 --- a/tests/Unit/Bounce/Service/LockServiceTest.php +++ b/tests/Unit/Domain/Messaging/Service/LockServiceTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service; -use PhpList\Core\Bounce\Service\LockService; +use PhpList\Core\Domain\Messaging\Service\LockService; use PhpList\Core\Domain\Messaging\Model\SendProcess; use PhpList\Core\Domain\Messaging\Repository\SendProcessRepository; use PhpList\Core\Domain\Messaging\Service\Manager\SendProcessManager; diff --git a/tests/Unit/Domain/Messaging/Service/Manager/BounceManagerTest.php b/tests/Unit/Domain/Messaging/Service/Manager/BounceManagerTest.php index 445dd240..3a07b0a0 100644 --- a/tests/Unit/Domain/Messaging/Service/Manager/BounceManagerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Manager/BounceManagerTest.php @@ -6,7 +6,7 @@ use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; -use PhpList\Core\Bounce\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Messaging\Model\UserMessageBounce; use PhpList\Core\Domain\Messaging\Repository\BounceRepository; diff --git a/tests/Unit/Domain/Messaging/Service/Manager/TemplateImageManagerTest.php b/tests/Unit/Domain/Messaging/Service/Manager/TemplateImageManagerTest.php index 63b3a4f9..296e0c1d 100644 --- a/tests/Unit/Domain/Messaging/Service/Manager/TemplateImageManagerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Manager/TemplateImageManagerTest.php @@ -117,7 +117,6 @@ public function testExtractAllImages(): void $result = $this->manager->extractAllImages($html); - $this->assertIsArray($result); $this->assertContains('image1.jpg', $result); $this->assertContains('https://example.com/image2.png', $result); } diff --git a/tests/Unit/Domain/Messaging/Service/Manager/TemplateManagerTest.php b/tests/Unit/Domain/Messaging/Service/Manager/TemplateManagerTest.php index efcb8b00..13af4168 100644 --- a/tests/Unit/Domain/Messaging/Service/Manager/TemplateManagerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Manager/TemplateManagerTest.php @@ -153,7 +153,6 @@ public function testListDefaultsReturnsDefaultTemplateDefinitions(): void $defaults = $this->manager->listDefaults(); - $this->assertIsArray($defaults); $this->assertCount(2, $defaults); $this->assertSame('system', $defaults[0]['key']); $this->assertSame('System', $defaults[0]['name']); diff --git a/tests/Unit/Domain/Messaging/Service/Manager/UserMessageForwardManagerTest.php b/tests/Unit/Domain/Messaging/Service/Manager/UserMessageForwardManagerTest.php index edf754c6..87573a40 100644 --- a/tests/Unit/Domain/Messaging/Service/Manager/UserMessageForwardManagerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Manager/UserMessageForwardManagerTest.php @@ -44,8 +44,7 @@ public function testCreatePersistsAndReturnsForwardWithExpectedFields(): void return $fwd->getUserId() === 42 && $fwd->getMessageId() === 7 && $fwd->getForward() === $expectedFriendEmail - && $fwd->getStatus() === $this->expectedStatus - && $fwd->getCreatedAt() !== null; + && $fwd->getStatus() === $this->expectedStatus; }) ); diff --git a/tests/Unit/Domain/Messaging/Service/Mapper/DefaultTemplateMapperTest.php b/tests/Unit/Domain/Messaging/Service/Mapper/DefaultTemplateMapperTest.php index 03f186bd..db1789b2 100644 --- a/tests/Unit/Domain/Messaging/Service/Mapper/DefaultTemplateMapperTest.php +++ b/tests/Unit/Domain/Messaging/Service/Mapper/DefaultTemplateMapperTest.php @@ -21,7 +21,6 @@ public function testListReturnsConfiguredDefaults(): void { $defaults = $this->mapper->list(); - $this->assertIsArray($defaults); $this->assertNotEmpty($defaults); $this->assertSame('system', $defaults[0]['key']); $this->assertSame('System', $defaults[0]['name']); @@ -50,7 +49,6 @@ public function testLoadContentReadsTemplateFile(): void { $content = $this->mapper->loadContent('system.html'); - $this->assertIsString($content); $this->assertNotSame('', $content); $this->assertStringContainsString('[CONTENT]', $content); } diff --git a/tests/Unit/Bounce/Service/MessageParserTest.php b/tests/Unit/Domain/Messaging/Service/MessageParserTest.php similarity index 95% rename from tests/Unit/Bounce/Service/MessageParserTest.php rename to tests/Unit/Domain/Messaging/Service/MessageParserTest.php index 35e60706..49b38615 100644 --- a/tests/Unit/Bounce/Service/MessageParserTest.php +++ b/tests/Unit/Domain/Messaging/Service/MessageParserTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service; -use PhpList\Core\Bounce\Service\MessageParser; +use PhpList\Core\Domain\Messaging\Service\MessageParser; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use PHPUnit\Framework\MockObject\MockObject; diff --git a/tests/Unit/Processor/AdvancedBounceRulesProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php similarity index 96% rename from tests/Unit/Processor/AdvancedBounceRulesProcessorTest.php rename to tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php index 7a57980d..91737353 100644 --- a/tests/Unit/Processor/AdvancedBounceRulesProcessorTest.php +++ b/tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Processor; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Processor; -use PhpList\Core\Bounce\Service\BounceActionResolver; -use PhpList\Core\Bounce\Service\Manager\BounceManager; -use PhpList\Core\Bounce\Service\Processor\AdvancedBounceRulesProcessor; +use PhpList\Core\Domain\Messaging\Service\BounceActionResolver; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Processor\AdvancedBounceRulesProcessor; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Messaging\Model\BounceRegex; use PhpList\Core\Domain\Messaging\Model\UserMessageBounce; diff --git a/tests/Unit/Processor/BounceDataProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/BounceDataProcessorTest.php similarity index 97% rename from tests/Unit/Processor/BounceDataProcessorTest.php rename to tests/Unit/Domain/Messaging/Service/Processor/BounceDataProcessorTest.php index d5d901c0..74d17e17 100644 --- a/tests/Unit/Processor/BounceDataProcessorTest.php +++ b/tests/Unit/Domain/Messaging/Service/Processor/BounceDataProcessorTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Processor; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Processor; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; -use PhpList\Core\Bounce\Service\Manager\BounceManager; -use PhpList\Core\Bounce\Service\Processor\BounceDataProcessor; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\Processor\BounceDataProcessor; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Messaging\Repository\MessageRepository; use PhpList\Core\Domain\Subscription\Model\Subscriber; diff --git a/tests/Unit/Processor/MboxBounceProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php similarity index 92% rename from tests/Unit/Processor/MboxBounceProcessorTest.php rename to tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php index a67235dd..9bf1c92f 100644 --- a/tests/Unit/Processor/MboxBounceProcessorTest.php +++ b/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Processor; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Processor; -use PhpList\Core\Bounce\Service\BounceProcessingServiceInterface; -use PhpList\Core\Bounce\Service\Processor\MboxBounceProcessor; +use PhpList\Core\Domain\Messaging\Service\BounceProcessingServiceInterface; +use PhpList\Core\Domain\Messaging\Service\Processor\MboxBounceProcessor; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use RuntimeException; diff --git a/tests/Unit/Processor/PopBounceProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/PopBounceProcessorTest.php similarity index 91% rename from tests/Unit/Processor/PopBounceProcessorTest.php rename to tests/Unit/Domain/Messaging/Service/Processor/PopBounceProcessorTest.php index d218edd0..d0141386 100644 --- a/tests/Unit/Processor/PopBounceProcessorTest.php +++ b/tests/Unit/Domain/Messaging/Service/Processor/PopBounceProcessorTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Processor; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Processor; -use PhpList\Core\Bounce\Service\BounceProcessingServiceInterface; -use PhpList\Core\Bounce\Service\Processor\PopBounceProcessor; +use PhpList\Core\Domain\Messaging\Service\BounceProcessingServiceInterface; +use PhpList\Core\Domain\Messaging\Service\Processor\PopBounceProcessor; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\InputInterface; diff --git a/tests/Unit/Processor/UnidentifiedBounceReprocessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessorTest.php similarity index 89% rename from tests/Unit/Processor/UnidentifiedBounceReprocessorTest.php rename to tests/Unit/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessorTest.php index 0e2d2254..3c740be9 100644 --- a/tests/Unit/Processor/UnidentifiedBounceReprocessorTest.php +++ b/tests/Unit/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessorTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Processor; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Processor; use DateTimeImmutable; -use PhpList\Core\Bounce\Service\Manager\BounceManager; -use PhpList\Core\Bounce\Service\MessageParser; -use PhpList\Core\Bounce\Service\Processor\BounceDataProcessor; -use PhpList\Core\Bounce\Service\Processor\UnidentifiedBounceReprocessor; +use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager; +use PhpList\Core\Domain\Messaging\Service\MessageParser; +use PhpList\Core\Domain\Messaging\Service\Processor\BounceDataProcessor; +use PhpList\Core\Domain\Messaging\Service\Processor\UnidentifiedBounceReprocessor; use PhpList\Core\Domain\Messaging\Model\Bounce; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Domain/Messaging/Service/SendRateLimiterTest.php b/tests/Unit/Domain/Messaging/Service/SendRateLimiterTest.php index e29f6929..b54265b7 100644 --- a/tests/Unit/Domain/Messaging/Service/SendRateLimiterTest.php +++ b/tests/Unit/Domain/Messaging/Service/SendRateLimiterTest.php @@ -64,12 +64,11 @@ public function testBatchLimitTriggersWaitMessageAndResetsCounters(): void // Next afterSend should increase the counter again without exception $limiter->afterSend(); - // Reaching here means no fatal due to internal counter/reset logic - $this->assertTrue(true); } public function testThrottleSleepsPerMessagePathIsCallable(): void { + $this->expectNotToPerformAssertions(); $this->ispProvider->method('load')->willReturn(new IspRestrictions(null, null, null)); $limiter = new SendRateLimiter( ispRestrictionsProvider: $this->ispProvider, @@ -89,6 +88,5 @@ public function testThrottleSleepsPerMessagePathIsCallable(): void if ($elapsed < 0.3) { $this->markTestIncomplete('Environment too fast to detect sleep; logic path executed.'); } - $this->assertTrue(true); } } diff --git a/tests/Unit/Bounce/Service/WebklexImapClientFactoryTest.php b/tests/Unit/Domain/Messaging/Service/WebklexImapClientFactoryTest.php similarity index 94% rename from tests/Unit/Bounce/Service/WebklexImapClientFactoryTest.php rename to tests/Unit/Domain/Messaging/Service/WebklexImapClientFactoryTest.php index c2b536cd..ca792e8f 100644 --- a/tests/Unit/Bounce/Service/WebklexImapClientFactoryTest.php +++ b/tests/Unit/Domain/Messaging/Service/WebklexImapClientFactoryTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\Core\Tests\Unit\Bounce\Service; +namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service; -use PhpList\Core\Bounce\Service\WebklexImapClientFactory; +use PhpList\Core\Domain\Messaging\Service\WebklexImapClientFactory; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Webklex\PHPIMAP\Client; diff --git a/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php b/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php index 40e1064a..09f3f2ff 100644 --- a/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php +++ b/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php @@ -53,8 +53,6 @@ public function testValidatesExistenceWithHttp200(): void ->willReturn(new Response(200)); $this->validator->validate(['https://example.com/image.jpg'], $context); - - $this->assertTrue(true); } public function testValidatesExistenceWithHttp404(): void diff --git a/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php b/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php index 5767f193..b61341db 100644 --- a/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php +++ b/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php @@ -21,20 +21,18 @@ protected function setUp(): void public function testSkipsValidationIfNotString(): void { + $this->expectNotToPerformAssertions(); $context = (new ValidationContext())->set('checkLinks', true); $this->validator->validate(['not', 'a', 'string'], $context); - - $this->assertTrue(true); } public function testSkipsValidationIfCheckLinksIsFalse(): void { + $this->expectNotToPerformAssertions(); $context = (new ValidationContext())->set('checkLinks', false); $this->validator->validate('Broken link', $context); - - $this->assertTrue(true); } public function testValidatesInvalidLinks(): void @@ -51,6 +49,7 @@ public function testValidatesInvalidLinks(): void public function testAllowsValidLinksAndPlaceholders(): void { + $this->expectNotToPerformAssertions(); $context = (new ValidationContext())->set('checkLinks', true); $html = '' . @@ -61,7 +60,5 @@ public function testAllowsValidLinksAndPlaceholders(): void ''; $this->validator->validate($html, $context); - - $this->assertTrue(true); } } diff --git a/tests/Unit/Domain/Subscription/MessageHandler/DynamicTableMessageHandlerTest.php b/tests/Unit/Domain/Subscription/MessageHandler/DynamicTableMessageHandlerTest.php index 8139e492..32603a02 100644 --- a/tests/Unit/Domain/Subscription/MessageHandler/DynamicTableMessageHandlerTest.php +++ b/tests/Unit/Domain/Subscription/MessageHandler/DynamicTableMessageHandlerTest.php @@ -7,6 +7,8 @@ use Doctrine\DBAL\Exception\TableExistsException; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Types\IntegerType; +use Doctrine\DBAL\Types\StringType; use InvalidArgumentException; use PhpList\Core\Domain\Subscription\Message\DynamicTableMessage; use PhpList\Core\Domain\Subscription\MessageHandler\DynamicTableMessageHandler; @@ -49,19 +51,19 @@ public function testInvokeCreatesTableWhenNotExists(): void // id column $idCol = $table->getColumn('id'); - $this->assertSame('integer', $idCol->getType()->getName()); + $this->assertInstanceOf(IntegerType::class, $idCol->getType()); $this->assertTrue($idCol->getAutoincrement()); $this->assertTrue($idCol->getNotnull()); // name column $nameCol = $table->getColumn('name'); - $this->assertSame('string', $nameCol->getType()->getName()); + $this->assertInstanceOf(StringType::class, $nameCol->getType()); $this->assertSame(255, $nameCol->getLength()); $this->assertFalse($nameCol->getNotnull()); // listorder column $orderCol = $table->getColumn('listorder'); - $this->assertSame('integer', $orderCol->getType()->getName()); + $this->assertInstanceOf(IntegerType::class, $orderCol->getType()); $this->assertFalse($orderCol->getNotnull()); $this->assertSame(0, $orderCol->getDefault()); @@ -104,8 +106,6 @@ public function testInvokeDoesNothingWhenTableAlreadyExists(): void $handler = new DynamicTableMessageHandler($this->schemaManager); $handler($message); - // reached without creating a table - $this->assertTrue(true); } public function testInvokeThrowsForInvalidTableName(): void @@ -125,7 +125,6 @@ public function testInvokeThrowsForInvalidTableName(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid list table name: ' . $invalidName); $handler($message); - $this->assertTrue(true); } public function testInvokeSwallowsTableExistsRace(): void @@ -151,6 +150,5 @@ public function testInvokeSwallowsTableExistsRace(): void // Should not throw despite the TableExistsException $handler($message); - $this->assertTrue(true); } } diff --git a/tests/Unit/Domain/Subscription/Model/SubscriberTest.php b/tests/Unit/Domain/Subscription/Model/SubscriberTest.php index d1aa848c..6a524308 100644 --- a/tests/Unit/Domain/Subscription/Model/SubscriberTest.php +++ b/tests/Unit/Domain/Subscription/Model/SubscriberTest.php @@ -56,11 +56,6 @@ public function testUpdateCreationDateSetsCreationDateToNow(): void self::assertSimilarDates(new \DateTime(), $this->subscriber->getCreatedAt()); } - public function testGetUpdatedAtInitiallyReturnsNotNull(): void - { - self::assertNotNull($this->subscriber->getUpdatedAt()); - } - public function testUpdateModificationDateSetsModificationDateToNow(): void { $this->subscriber->updateUpdatedAt(); diff --git a/tests/Unit/Domain/Subscription/Model/SubscriptionTest.php b/tests/Unit/Domain/Subscription/Model/SubscriptionTest.php index e0148a83..797210bb 100644 --- a/tests/Unit/Domain/Subscription/Model/SubscriptionTest.php +++ b/tests/Unit/Domain/Subscription/Model/SubscriptionTest.php @@ -35,11 +35,6 @@ public function testIsDomainModel(): void self::assertInstanceOf(DomainModel::class, $this->subject); } - public function testGetSubscriberInitiallyReturnsNull(): void - { - self::assertNull($this->subject->getSubscriber()); - } - public function testSetSubscriberSetsSubscriber(): void { $model = new Subscriber('test@example.com'); @@ -48,11 +43,6 @@ public function testSetSubscriberSetsSubscriber(): void self::assertSame($model, $this->subject->getSubscriber()); } - public function testGetSubscriberListInitiallyReturnsNull(): void - { - self::assertNull($this->subject->getSubscriberList()); - } - public function testSetSubscriberListSetsSubscriberList(): void { $model = new SubscriberList(); @@ -66,11 +56,6 @@ public function testGetCreatedAtInitiallyReturnsCurrentTime(): void self::assertSimilarDates(new DateTime(), $this->subject->getCreatedAt()); } - public function testGetUpdatedAtInitiallyReturnsNull(): void - { - self::assertNull($this->subject->getUpdatedAt()); - } - public function testUpdateModificationDateSetsModificationDateToNow(): void { $this->subject->updateUpdatedAt(); diff --git a/tests/Unit/Domain/Subscription/Service/Manager/AttributeDefinitionManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/AttributeDefinitionManagerTest.php index f109f1c4..efad3937 100644 --- a/tests/Unit/Domain/Subscription/Service/Manager/AttributeDefinitionManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/Manager/AttributeDefinitionManagerTest.php @@ -5,7 +5,7 @@ namespace PhpList\Core\Tests\Unit\Domain\Subscription\Service\Manager; use PhpList\Core\Domain\Common\Model\AttributeTypeEnum; -use PhpList\Core\Domain\Subscription\Exception\AttributeDefinitionCreationException; +use PhpList\Core\Domain\Common\Exception\AttributeDefinitionCreationException; use PhpList\Core\Domain\Subscription\Model\Dto\AttributeDefinitionDto; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; @@ -189,7 +189,5 @@ public function testDeleteAttributeDefinition(): void $repository->expects($this->once())->method('remove')->with($attribute); $manager->delete($attribute); - - $this->assertTrue(true); } } diff --git a/tests/Unit/Domain/Subscription/Service/Manager/DynamicListAttrManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/DynamicListAttrManagerTest.php index c51cb81d..d39bc28a 100644 --- a/tests/Unit/Domain/Subscription/Service/Manager/DynamicListAttrManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/Manager/DynamicListAttrManagerTest.php @@ -49,8 +49,6 @@ public function testInsertOptionsSkipsEmpty(): void // Empty array should be a no-op (no DB calls) $this->listAttrRepo->expects($this->never())->method('transactional'); $manager->insertOptions('colors', []); - // if we got here, expectations were met - $this->assertTrue(true); } public function testInsertOptionsSkipsDuplicatesAndAssignsOrder(): void diff --git a/tests/Unit/Domain/Subscription/Service/Manager/DynamicListAttrTablesManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/DynamicListAttrTablesManagerTest.php index a9166799..eb3c6fda 100644 --- a/tests/Unit/Domain/Subscription/Service/Manager/DynamicListAttrTablesManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/Manager/DynamicListAttrTablesManagerTest.php @@ -78,6 +78,5 @@ public function testCreateOptionsTableIfNotExistsDispatchesMessage(): void $manager = $this->makeManager(); $manager->createOptionsTableIfNotExists('sizes'); $manager->createOptionsTableIfNotExists('sizes'); - $this->assertTrue(true); } } diff --git a/tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php index 5bfc65bb..7c05dca2 100644 --- a/tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php @@ -355,7 +355,7 @@ public function testSyncPageDataCallsCopyToConfigWhenFeatureIsEnabled(): void $this->configMigrationService ->expects($this->once()) ->method('copyToConfig') - ->with(page: $this->page, data: $data); + ->with($this->page, $data); $this->manager->syncPageData($data, $this->page); } diff --git a/tests/Unit/Domain/Subscription/Service/Manager/SubscriberAttributeManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/SubscriberAttributeManagerTest.php index e0632df9..3cab283e 100644 --- a/tests/Unit/Domain/Subscription/Service/Manager/SubscriberAttributeManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/Manager/SubscriberAttributeManagerTest.php @@ -146,7 +146,5 @@ public function testDeleteSubscriberAttribute(): void translator: new Translator('en'), ); $manager->delete($attribute); - - self::assertTrue(true); } } diff --git a/tests/Unit/Domain/Subscription/Service/Manager/SubscriberHistoryManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/SubscriberHistoryManagerTest.php index aaac6847..f28dc08a 100644 --- a/tests/Unit/Domain/Subscription/Service/Manager/SubscriberHistoryManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/Manager/SubscriberHistoryManagerTest.php @@ -71,6 +71,5 @@ public function testGetHistoryReturnsEmptyArrayWhenRepositoryReturnsEmptyArray() $result = $this->subscriptionHistoryService->getHistory($lastId, $limit, $filter); $this->assertSame($expectedResult, $result); - $this->assertEmpty($result); } } diff --git a/tests/Unit/Domain/Subscription/Service/Manager/SubscriberListManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/SubscriberListManagerTest.php index 124ace08..44247367 100644 --- a/tests/Unit/Domain/Subscription/Service/Manager/SubscriberListManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/Manager/SubscriberListManagerTest.php @@ -59,7 +59,6 @@ public function testGetPaginated(): void $result = $this->manager->getPaginated(0, 1); - $this->assertIsArray($result); $this->assertCount(1, $result); $this->assertSame($list, $result[0]); } diff --git a/tests/Unit/Domain/Subscription/Service/Manager/SubscriptionManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/SubscriptionManagerTest.php index a7840480..9b575d38 100644 --- a/tests/Unit/Domain/Subscription/Service/Manager/SubscriptionManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/Manager/SubscriptionManagerTest.php @@ -113,7 +113,6 @@ public function testGetSubscriberListMembersReturnsList(): void $result = $this->manager->getSubscriberListMembers($subscriberList); - $this->assertIsArray($result); $this->assertCount(1, $result); $this->assertInstanceOf(Subscriber::class, $result[0]); } diff --git a/tests/Unit/Domain/Subscription/Service/Provider/SubscriberProviderTest.php b/tests/Unit/Domain/Subscription/Service/Provider/SubscriberProviderTest.php index ee263b73..a68576a1 100644 --- a/tests/Unit/Domain/Subscription/Service/Provider/SubscriberProviderTest.php +++ b/tests/Unit/Domain/Subscription/Service/Provider/SubscriberProviderTest.php @@ -46,7 +46,6 @@ public function testGetSubscribersForMessageWithNoListsReturnsEmptyArray(): void $message, ); - $this->assertIsArray($result); $this->assertEmpty($result); } @@ -69,7 +68,6 @@ public function testGetSubscribersForMessageWithOneListButNoSubscribersReturnsEm $this->createMock(CampaignProcessorMessageInterface::class), $message, ); - $this->assertIsArray($result); $this->assertEmpty($result); } @@ -97,7 +95,6 @@ public function testGetSubscribersForMessageWithOneListAndSubscribersReturnsSubs new CampaignProcessorMessage(1), $message, ); - $this->assertIsArray($result); $this->assertCount(2, $result); $this->assertSame($subscriber1, $result[0]); $this->assertSame($subscriber2, $result[1]); @@ -131,7 +128,6 @@ public function testGetSubscribersForMessageWithMultipleListsReturnsUniqueSubscr $this->createMock(CampaignProcessorMessageInterface::class), $message, ); - $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertContains($subscriber1, $result); diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberCsvExporterTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberCsvExporterTest.php index 91605f73..3c7ae4b3 100644 --- a/tests/Unit/Domain/Subscription/Service/SubscriberCsvExporterTest.php +++ b/tests/Unit/Domain/Subscription/Service/SubscriberCsvExporterTest.php @@ -98,8 +98,8 @@ public function testExportToCsvWithFilterReturnsStreamedResponse(): void $this->assertInstanceOf(Response::class, $response); $this->assertSame('text/csv; charset=utf-8', $response->headers->get('Content-Type')); $this->assertStringContainsString( - needle: 'attachment; filename=subscribers_export_', - haystack: $response->headers->get('Content-Disposition') + 'attachment; filename=subscribers_export_', + $response->headers->get('Content-Disposition') ); } @@ -145,8 +145,8 @@ public function testExportToCsvWithoutFilterCreatesDefaultFilter(): void $this->assertInstanceOf(Response::class, $response); $this->assertSame('text/csv; charset=utf-8', $response->headers->get('Content-Type')); $this->assertStringContainsString( - needle: 'attachment; filename=subscribers_export_', - haystack: $response->headers->get('Content-Disposition') + 'attachment; filename=subscribers_export_', + $response->headers->get('Content-Disposition') ); } } diff --git a/tests/Unit/Domain/Subscription/Validator/AttributeTypeValidatorTest.php b/tests/Unit/Domain/Subscription/Validator/AttributeTypeValidatorTest.php index 7f31f772..054334ba 100644 --- a/tests/Unit/Domain/Subscription/Validator/AttributeTypeValidatorTest.php +++ b/tests/Unit/Domain/Subscription/Validator/AttributeTypeValidatorTest.php @@ -20,11 +20,10 @@ protected function setUp(): void public function testValidatesValidType(): void { + $this->expectNotToPerformAssertions(); $this->validator->validate('textline'); $this->validator->validate('checkbox'); $this->validator->validate('date'); - - $this->assertTrue(true); } public function testThrowsExceptionForInvalidType(): void @@ -32,7 +31,6 @@ public function testThrowsExceptionForInvalidType(): void $this->expectException(ValidatorException::class); $this->validator->validate('invalid_type'); - $this->assertTrue(true); } public function testThrowsExceptionForNonStringValue(): void @@ -40,6 +38,5 @@ public function testThrowsExceptionForNonStringValue(): void $this->expectException(ValidatorException::class); $this->validator->validate(123); - $this->assertTrue(true); } } diff --git a/tests/Unit/Security/HashGeneratorTest.php b/tests/Unit/Security/HashGeneratorTest.php deleted file mode 100644 index b8bd956b..00000000 --- a/tests/Unit/Security/HashGeneratorTest.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ -class HashGeneratorTest extends TestCase -{ - private HashGenerator $subject; - - protected function setUp(): void - { - $this->subject = new HashGenerator(); - } - - public function testCreatePasswordHashCreates64CharacterHash(): void - { - $hash = $this->subject->createPasswordHash('Portal'); - self::assertMatchesRegularExpression('/^[a-z0-9]{64}$/', $hash); - } - - public function testCreatePasswordHashCalledTwoTimesWithSamePasswordCreatesSameHash(): void - { - $password = 'Aperture Science'; - - $hash1 = $this->subject->createPasswordHash($password); - $hash2 = $this->subject->createPasswordHash($password); - - self::assertSame($hash1, $hash2); - } - - public function testCreatePasswordHashCalledTwoTimesWithDifferentPasswordsCreatesDifferentHashes(): void - { - $hash1 = $this->subject->createPasswordHash('Mel'); - $hash2 = $this->subject->createPasswordHash('Cave Johnson'); - - self::assertNotSame($hash1, $hash2); - } -}