Merge branch 'trunk' into PCP-327-prevent-subscription-from-being-

This commit is contained in:
dinamiko 2021-10-14 10:34:26 +02:00
commit ab0ed12495
30 changed files with 2820 additions and 74 deletions

View file

@ -25,4 +25,6 @@ ADMIN_USER=admin
ADMIN_PASS=admin
ADMIN_EMAIL=me@my.com
#NGROK_HOST=mydomain.ngrok.io
COMPOSER_MEMORY_LIMIT=1.5G

View file

@ -30,5 +30,8 @@ jobs:
- name: Run PHPUnit
run: vendor/bin/phpunit
- name: Psalm
run: ./vendor/bin/psalm --show-info=false --threads=8 --diff
- name: Run PHPCS
run: ./vendor/bin/phpcs --runtime-set ignore_warnings_on_exit 1 src modules woocommerce-paypal-payments.php --extensions=php

View file

@ -21,6 +21,7 @@ PayPal's latest complete payments processing solution. Accept PayPal, Pay Later,
1. `$ composer install`
2. `$ ./vendor/bin/phpunit`
3. `$ ./vendor/bin/phpcs`
4. `$ ./vendor/bin/psalm`
### Docker
@ -44,6 +45,26 @@ After some changes in `.env` (such as PHP, WP versions) you may need to rebuild
See [package.json](/package.json) for other useful commands.
### Webhooks
For testing webhooks locally, follow these steps to set up ngrok:
0. Install [ngrok](https://ngrok.com/).
1. Run
```
ngrok http -host-header=rewrite wc-pp.myhost
```
2. In your environment variables (accessible to the web server), add `NGROK_HOST` with the host that you got from `ngrok`, like `abcd1234.ngrok.io`.
- For the Docker environment: set `NGROK_HOST` in the `.env` file and restart the web server. (`yarn run docker:stop && yarn run docker:start`)
3. Complete onboarding or resubscribe webhooks on the Webhooks Status page.
Currently, ngrok is used only for the webhook listening URL.
The URLs displayed on the WordPress pages, used in redirects, etc. will still remain local.
## Building a release package
If you want to build a release package

View file

@ -17,7 +17,10 @@
"require-dev": {
"woocommerce/woocommerce-sniffs": "^0.1.0",
"phpunit/phpunit": "^7.0 | ^8.0 | ^9.0",
"brain/monkey": "^2.4"
"brain/monkey": "^2.4",
"php-stubs/wordpress-stubs": "^5.0@stable",
"php-stubs/woocommerce-stubs": "^5.0@stable",
"vimeo/psalm": "^4.0"
},
"autoload": {
"psr-4": {

1546
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -32,6 +32,7 @@ services:
WP_DOMAIN: ${WP_DOMAIN}
WP_TITLE: ${WP_TITLE}
WC_VERSION: $WC_VERSION
NGROK_HOST: ${NGROK_HOST}
volumes:
- wordpress:${DOCROOT_PATH}

View file

@ -221,10 +221,10 @@ return array(
'api.factory.webhook' => static function ( ContainerInterface $container ): WebhookFactory {
return new WebhookFactory();
},
'api.factory.webhook-event' => static function ( $container ): WebhookEventFactory {
'api.factory.webhook-event' => static function ( ContainerInterface $container ): WebhookEventFactory {
return new WebhookEventFactory();
},
'api.factory.capture' => static function ( $container ): CaptureFactory {
'api.factory.capture' => static function ( ContainerInterface $container ): CaptureFactory {
$amount_factory = $container->get( 'api.factory.amount' );
return new CaptureFactory( $amount_factory );

View file

@ -164,7 +164,7 @@ class Capture {
* @return array
*/
public function to_array() : array {
$data = array(
$data = array(
'id' => $this->id(),
'status' => $this->status()->name(),
'amount' => $this->amount()->to_array(),
@ -173,8 +173,9 @@ class Capture {
'invoice_id' => $this->invoice_id(),
'custom_id' => $this->custom_id(),
);
if ( $this->status()->details() ) {
$data['status_details'] = array( 'reason' => $this->status()->details()->reason() );
$details = $this->status()->details();
if ( $details ) {
$data['status_details'] = array( 'reason' => $details->reason() );
}
return $data;
}

View file

@ -40,9 +40,9 @@ class Webhook {
/**
* Webhook constructor.
*
* @param string $url The URL of the webhook.
* @param string[] $event_types The associated event types.
* @param string $id The id of the webhook.
* @param string $url The URL of the webhook.
* @param stdClass[] $event_types The associated event types.
* @param string $id The id of the webhook.
*/
public function __construct( string $url, array $event_types, string $id = '' ) {
$this->url = $url;

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
@ -20,15 +21,15 @@ class WebhookFactory {
/**
* Returns a webhook for a URL with an array of event types associated to this URL.
*
* @param string $url The URL.
* @param array $event_types The event types to which this URL listens to.
* @param string $url The URL.
* @param string[] $event_types The event types to which this URL listens to.
*
* @return Webhook
*/
public function for_url_and_events( string $url, array $event_types ): Webhook {
$event_types = array_map(
static function ( string $type ): array {
return array( 'name' => $type );
static function ( string $type ): stdClass {
return (object) array( 'name' => $type );
},
$event_types
);
@ -52,12 +53,12 @@ class WebhookFactory {
/**
* Returns a Webhook based of a PayPal JSON response.
*
* @param \stdClass $data The JSON object.
* @param stdClass $data The JSON object.
*
* @return Webhook
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( \stdClass $data ): Webhook {
public function from_paypal_response( stdClass $data ): Webhook {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'No id for webhook given.', 'woocommerce-paypal-payments' )

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat;
use Psr\Container\ContainerInterface;
return array(
'compat.ppec.mock-gateway' => static function( $container ) {
@ -23,7 +25,7 @@ return array(
return new PPEC\MockGateway( $title );
},
'compat.ppec.subscriptions-handler' => static function ( $container ) {
'compat.ppec.subscriptions-handler' => static function ( ContainerInterface $container ) {
$ppcp_renewal_handler = $container->get( 'subscription.renewal-handler' );
$gateway = $container->get( 'compat.ppec.mock-gateway' );

View file

@ -10,7 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\StatusReport;
return array(
'status-report.renderer' => static function ( $container ): Renderer {
'status-report.renderer' => static function (): Renderer {
return new Renderer();
},
);

View file

@ -9,17 +9,18 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Vaulting\Assets\MyAccountPaymentsAssets;
use WooCommerce\PayPalCommerce\Vaulting\Endpoint\DeletePaymentTokenEndpoint;
return array(
'vaulting.module-url' => static function ( $container ): string {
'vaulting.module-url' => static function ( ContainerInterface $container ): string {
return plugins_url(
'/modules/ppcp-vaulting/',
dirname( __FILE__, 3 ) . '/woocommerce-paypal-payments.php'
);
},
'vaulting.assets.myaccount-payments' => function( $container ) : MyAccountPaymentsAssets {
'vaulting.assets.myaccount-payments' => function( ContainerInterface $container ) : MyAccountPaymentsAssets {
return new MyAccountPaymentsAssets(
$container->get( 'vaulting.module-url' )
);
@ -27,12 +28,12 @@ return array(
'vaulting.payment-tokens-renderer' => static function (): PaymentTokensRenderer {
return new PaymentTokensRenderer();
},
'vaulting.repository.payment-token' => static function ( $container ): PaymentTokenRepository {
'vaulting.repository.payment-token' => static function ( ContainerInterface $container ): PaymentTokenRepository {
$factory = $container->get( 'api.factory.payment-token' );
$endpoint = $container->get( 'api.endpoint.payment-token' );
return new PaymentTokenRepository( $factory, $endpoint );
},
'vaulting.endpoint.delete' => function( $container ) : DeletePaymentTokenEndpoint {
'vaulting.endpoint.delete' => function( ContainerInterface $container ) : DeletePaymentTokenEndpoint {
return new DeletePaymentTokenEndpoint(
$container->get( 'vaulting.repository.payment-token' ),
$container->get( 'button.request-data' ),

View file

@ -20,15 +20,15 @@ use Psr\Log\LoggerInterface;
return array(
'api.merchant_email' => static function ( $container ): string {
'api.merchant_email' => static function ( ContainerInterface $container ): string {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'merchant_email' ) ? (string) $settings->get( 'merchant_email' ) : '';
},
'api.merchant_id' => static function ( $container ): string {
'api.merchant_id' => static function ( ContainerInterface $container ): string {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'merchant_id' ) ? (string) $settings->get( 'merchant_id' ) : '';
},
'api.partner_merchant_id' => static function ( $container ): string {
'api.partner_merchant_id' => static function ( ContainerInterface $container ): string {
$environment = $container->get( 'onboarding.environment' );
/**
@ -39,20 +39,20 @@ return array(
return $environment->current_environment_is( Environment::SANDBOX ) ?
(string) $container->get( 'api.partner_merchant_id-sandbox' ) : (string) $container->get( 'api.partner_merchant_id-production' );
},
'api.key' => static function ( $container ): string {
'api.key' => static function ( ContainerInterface $container ): string {
$settings = $container->get( 'wcgateway.settings' );
$key = $settings->has( 'client_id' ) ? (string) $settings->get( 'client_id' ) : '';
return $key;
},
'api.secret' => static function ( $container ): string {
'api.secret' => static function ( ContainerInterface $container ): string {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'client_secret' ) ? (string) $settings->get( 'client_secret' ) : '';
},
'api.prefix' => static function ( $container ): string {
'api.prefix' => static function ( ContainerInterface $container ): string {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'prefix' ) ? (string) $settings->get( 'prefix' ) : 'WC-';
},
'api.endpoint.order' => static function ( $container ): OrderEndpoint {
'api.endpoint.order' => static function ( ContainerInterface $container ): OrderEndpoint {
$order_factory = $container->get( 'api.factory.order' );
$patch_collection_factory = $container->get( 'api.factory.patch-collection-factory' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
@ -87,7 +87,7 @@ return array(
$bn_code
);
},
'woocommerce.logger.woocommerce' => function ( $container ): LoggerInterface {
'woocommerce.logger.woocommerce' => function ( ContainerInterface $container ): LoggerInterface {
$settings = $container->get( 'wcgateway.settings' );
if ( ! function_exists( 'wc_get_logger' ) || ! $settings->has( 'logging_enabled' ) || ! $settings->get( 'logging_enabled' ) ) {
return new NullLogger();

View file

@ -123,13 +123,13 @@ return array(
$settings = $container->get( 'wcgateway.settings' );
return new DisableGateways( $session_handler, $settings );
},
'wcgateway.is-wc-payments-page' => static function ( $container ): bool {
'wcgateway.is-wc-payments-page' => static function ( ContainerInterface $container ): bool {
$page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
$tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : '';
return 'wc-settings' === $page && 'checkout' === $tab;
},
'wcgateway.is-ppcp-settings-page' => static function ( $container ): bool {
'wcgateway.is-ppcp-settings-page' => static function ( ContainerInterface $container ): bool {
if ( ! $container->get( 'wcgateway.is-wc-payments-page' ) ) {
return false;
}
@ -138,7 +138,7 @@ return array(
return in_array( $section, array( PayPalGateway::ID, CreditCardGateway::ID, WebhooksStatusPage::ID ), true );
},
'wcgateway.current-ppcp-settings-page-id' => static function ( $container ): string {
'wcgateway.current-ppcp-settings-page-id' => static function ( ContainerInterface $container ): string {
if ( ! $container->get( 'wcgateway.is-ppcp-settings-page' ) ) {
return '';
}
@ -165,7 +165,7 @@ return array(
return new DccWithoutPayPalAdminNotice( $state, $settings, $is_payments_page, $is_ppcp_settings_page );
},
'wcgateway.notice.authorize-order-action' =>
static function ( $container ): AuthorizeOrderActionNotice {
static function ( ContainerInterface $container ): AuthorizeOrderActionNotice {
return new AuthorizeOrderActionNotice();
},
'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer {
@ -1983,10 +1983,10 @@ return array(
dirname( __FILE__, 3 ) . '/woocommerce-paypal-payments.php'
);
},
'wcgateway.relative-path' => static function( $container ): string {
'wcgateway.relative-path' => static function( ContainerInterface $container ): string {
return 'modules/ppcp-wc-gateway/';
},
'wcgateway.absolute-path' => static function( $container ): string {
'wcgateway.absolute-path' => static function( ContainerInterface $container ): string {
return plugin_dir_path(
dirname( __FILE__, 3 ) . '/woocommerce-paypal-payments.php'
) .
@ -2003,15 +2003,15 @@ return array(
);
},
'wcgateway.transaction-url-sandbox' => static function ( $container ): string {
'wcgateway.transaction-url-sandbox' => static function ( ContainerInterface $container ): string {
return 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s';
},
'wcgateway.transaction-url-live' => static function ( $container ): string {
'wcgateway.transaction-url-live' => static function ( ContainerInterface $container ): string {
return 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s';
},
'wcgateway.transaction-url-provider' => static function ( $container ): TransactionUrlProvider {
'wcgateway.transaction-url-provider' => static function ( ContainerInterface $container ): TransactionUrlProvider {
$sandbox_url_base = $container->get( 'wcgateway.transaction-url-sandbox' );
$live_url_base = $container->get( 'wcgateway.transaction-url-live' );
@ -2025,7 +2025,7 @@ return array(
return new DCCProductStatus( $settings, $partner_endpoint );
},
'button.helper.messages-disclaimers' => static function ( $container ): MessagesDisclaimers {
'button.helper.messages-disclaimers' => static function ( ContainerInterface $container ): MessagesDisclaimers {
return new MessagesDisclaimers();
},
);

View file

@ -55,8 +55,9 @@ trait PaymentsStatusHandlingTrait {
): void {
$status = $capture->status();
if ( $status->details() ) {
$this->add_status_details_note( $wc_order, $status->name(), $status->details()->text() );
$details = $status->details();
if ( $details ) {
$this->add_status_details_note( $wc_order, $status->name(), $details->text() );
}
switch ( $status->name() ) {
@ -95,8 +96,9 @@ trait PaymentsStatusHandlingTrait {
): void {
$status = $authorization->status();
if ( $status->details() ) {
$this->add_status_details_note( $wc_order, $status->name(), $status->details()->text() );
$details = $status->details();
if ( $details ) {
$this->add_status_details_note( $wc_order, $status->name(), $details->text() );
}
switch ( $status->name() ) {

View file

@ -148,9 +148,9 @@ class WCGatewayModule implements ModuleInterface {
/**
* Registers the payment gateways.
*
* @param ContainerInterface|null $container The container.
* @param ContainerInterface $container The container.
*/
private function register_payment_gateways( ContainerInterface $container = null ) {
private function register_payment_gateways( ContainerInterface $container ) {
add_filter(
'woocommerce_payment_gateways',

View file

@ -72,7 +72,7 @@ return array(
);
},
'webhook.current' => function( $container ) : ?Webhook {
'webhook.current' => function( ContainerInterface $container ) : ?Webhook {
$data = (array) get_option( WebhookRegistrar::KEY, array() );
if ( empty( $data ) ) {
return null;
@ -91,18 +91,18 @@ return array(
}
},
'webhook.is-registered' => function( $container ) : bool {
'webhook.is-registered' => function( ContainerInterface $container ) : bool {
return $container->get( 'webhook.current' ) !== null;
},
'webhook.status.registered-webhooks' => function( $container ) : array {
'webhook.status.registered-webhooks' => function( ContainerInterface $container ) : array {
$endpoint = $container->get( 'api.endpoint.webhook' );
assert( $endpoint instanceof WebhookEndpoint );
return $endpoint->list();
},
'webhook.status.registered-webhooks-data' => function( $container ) : array {
'webhook.status.registered-webhooks-data' => function( ContainerInterface $container ) : array {
$empty_placeholder = __( 'No webhooks found.', 'woocommerce-paypal-payments' );
$webhooks = array();
@ -139,7 +139,7 @@ return array(
);
},
'webhook.status.simulation' => function( $container ) : WebhookSimulation {
'webhook.status.simulation' => function( ContainerInterface $container ) : WebhookSimulation {
$webhook_endpoint = $container->get( 'api.endpoint.webhook' );
$webhook = $container->get( 'webhook.current' );
return new WebhookSimulation(
@ -150,13 +150,13 @@ return array(
);
},
'webhook.status.assets' => function( $container ) : WebhooksStatusPageAssets {
'webhook.status.assets' => function( ContainerInterface $container ) : WebhooksStatusPageAssets {
return new WebhooksStatusPageAssets(
$container->get( 'webhook.module-url' )
);
},
'webhook.endpoint.resubscribe' => static function ( $container ) : ResubscribeEndpoint {
'webhook.endpoint.resubscribe' => static function ( ContainerInterface $container ) : ResubscribeEndpoint {
$registrar = $container->get( 'webhook.registrar' );
$request_data = $container->get( 'button.request-data' );
@ -166,7 +166,7 @@ return array(
);
},
'webhook.endpoint.simulate' => static function ( $container ) : SimulateEndpoint {
'webhook.endpoint.simulate' => static function ( ContainerInterface $container ) : SimulateEndpoint {
$simulation = $container->get( 'webhook.status.simulation' );
$request_data = $container->get( 'button.request-data' );
@ -175,7 +175,7 @@ return array(
$request_data
);
},
'webhook.endpoint.simulation-state' => static function ( $container ) : SimulationStateEndpoint {
'webhook.endpoint.simulation-state' => static function ( ContainerInterface $container ) : SimulationStateEndpoint {
$simulation = $container->get( 'webhook.status.simulation' );
return new SimulationStateEndpoint(
@ -183,7 +183,7 @@ return array(
);
},
'webhook.module-url' => static function ( $container ): string {
'webhook.module-url' => static function ( ContainerInterface $container ): string {
return plugins_url(
'/modules/ppcp-webhooks/',
dirname( __FILE__, 3 ) . '/woocommerce-paypal-payments.php'

View file

@ -50,7 +50,7 @@ class CheckoutOrderApproved implements RequestHandler {
/**
* The event types a handler handles.
*
* @return array
* @return string[]
*/
public function event_types(): array {
return array(

View file

@ -39,7 +39,7 @@ class CheckoutOrderCompleted implements RequestHandler {
/**
* The event types a handler handles.
*
* @return array
* @return string[]
*/
public function event_types(): array {
return array(

View file

@ -40,7 +40,7 @@ class PaymentCaptureCompleted implements RequestHandler {
/**
* The event types a handler handles.
*
* @return array
* @return string[]
*/
public function event_types(): array {
return array( 'PAYMENT.CAPTURE.COMPLETED' );

View file

@ -39,7 +39,7 @@ class PaymentCaptureRefunded implements RequestHandler {
/**
* The event types a handler handles.
*
* @return array
* @return string[]
*/
public function event_types(): array {
return array( 'PAYMENT.CAPTURE.REFUNDED' );

View file

@ -42,7 +42,7 @@ class PaymentCaptureReversed implements RequestHandler {
/**
* The event types a handler handles.
*
* @return array
* @return string[]
*/
public function event_types(): array {
return array(

View file

@ -17,7 +17,7 @@ interface RequestHandler {
/**
* The event types a handler handles.
*
* @return array
* @return string[]
*/
public function event_types(): array;

View file

@ -230,13 +230,25 @@ class IncomingWebhookEndpoint {
* @return string
*/
public function url(): string {
return rest_url( self::NAMESPACE . '/' . self::ROUTE );
$url = rest_url( self::NAMESPACE . '/' . self::ROUTE );
$url = str_replace( 'http://', 'https://', $url );
$ngrok_host = getenv( 'NGROK_HOST' );
if ( $ngrok_host ) {
$host = wp_parse_url( $url, PHP_URL_HOST );
if ( $host ) {
$url = str_replace( $host, $ngrok_host, $url );
}
}
return $url;
}
/**
* Returns the event types, which are handled by the endpoint.
*
* @return array
* @return string[]
*/
public function handled_event_types(): array {
$event_types = array();

View file

@ -47,14 +47,14 @@ class WebhooksStatusPageAssets {
'ppcp-webhooks-status-page-style',
$this->module_url . '/assets/css/status-page.css',
array(),
1
'1'
);
wp_register_script(
'ppcp-webhooks-status-page',
$this->module_url . '/assets/js/status-page.js',
array(),
1,
'1',
true
);

View file

@ -78,7 +78,7 @@ class WebhookSimulation {
*
* @throws Exception If failed to start simulation.
*/
public function start() {
public function start(): void {
if ( ! $this->webhook ) {
throw new Exception( 'Webhooks not registered' );
}

View file

@ -28,7 +28,7 @@
"docker:build-js": "docker-compose run --rm build yarn run build:dev",
"docker:composer-update": "docker-compose run --rm composer composer update && docker-compose run --rm composer composer update --lock",
"docker:test": "docker-compose run --rm test vendor/bin/phpunit",
"docker:lint": "docker-compose run --rm test vendor/bin/phpcs --parallel=8 -s",
"docker:lint": "docker-compose run --rm test sh -c 'vendor/bin/phpcs --parallel=8 -s && vendor/bin/psalm --show-info=false --threads=8 --diff'",
"docker:fix-lint": "docker-compose run --rm test vendor/bin/phpcbf",

1012
psalm-baseline.xml Normal file

File diff suppressed because it is too large Load diff

153
psalm.xml.dist Normal file
View file

@ -0,0 +1,153 @@
<?xml version="1.0"?>
<psalm
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
xmlns:xi="http://www.w3.org/2001/XInclude"
totallyTyped="false"
useDocblockTypes="true"
usePhpDocMethodsWithoutMagicCall="false"
strictBinaryOperands="true"
rememberPropertyAssignmentsAfterCall="true"
allowPhpStormGenerics="true"
allowStringToStandInForClass="false"
memoizeMethodCallResults="false"
hoistConstants="false"
addParamDefaultToDocblockType="false"
checkForThrowsDocblock="true"
checkForThrowsInGlobalScope="false"
ignoreInternalFunctionFalseReturn="false"
ignoreInternalFunctionNullReturn="false"
throwExceptionOnError="false"
hideExternalErrors="true"
allowFileIncludes="true"
errorBaseline="psalm-baseline.xml"
>
<projectFiles>
<directory name="src"/>
<directory name="modules" />
</projectFiles>
<stubs>
<file name="vendor/php-stubs/wordpress-stubs/wordpress-stubs.php"/>
<file name="vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php"/>
</stubs>
<issueHandlers>
<ConflictingReferenceConstraint errorLevel="error"/>
<ContinueOutsideLoop errorLevel="error"/>
<DuplicateArrayKey errorLevel="error"/>
<DuplicateClass errorLevel="error"/>
<DuplicateFunction errorLevel="error"/>
<DuplicateMethod errorLevel="error"/>
<DuplicateParam errorLevel="error"/>
<EmptyArrayAccess errorLevel="error"/>
<FalsableReturnStatement errorLevel="error"/>
<FalseOperand errorLevel="error"/>
<ForbiddenCode errorLevel="error"/>
<ForbiddenEcho errorLevel="error"/>
<InaccessibleClassConstant errorLevel="error"/>
<InaccessibleMethod errorLevel="error"/>
<InterfaceInstantiation errorLevel="error"/>
<InaccessibleProperty errorLevel="error"/>
<InternalClass errorLevel="error"/>
<InternalMethod errorLevel="error"/>
<InternalProperty errorLevel="error"/>
<InvalidArgument errorLevel="error"/>
<InvalidArrayAccess errorLevel="error"/>
<InvalidArrayAssignment errorLevel="error"/>
<InvalidArrayOffset errorLevel="error"/>
<InvalidCast errorLevel="error"/>
<InvalidCatch errorLevel="error"/>
<InvalidClass errorLevel="error"/>
<InvalidClone errorLevel="error"/>
<InvalidFalsableReturnType errorLevel="error"/>
<InvalidThrow errorLevel="error"/>
<InvalidToString errorLevel="error"/>
<LoopInvalidation errorLevel="error"/>
<InvalidNullableReturnType errorLevel="error"/>
<LessSpecificReturnType errorLevel="error"/>
<InvalidGlobal errorLevel="error"/>
<InvalidIterator errorLevel="error"/>
<InvalidMethodCall errorLevel="error"/>
<InvalidFunctionCall errorLevel="error"/>
<ImplicitToStringCast errorLevel="error"/>
<ImplementedReturnTypeMismatch errorLevel="error"/>
<InvalidParamDefault errorLevel="error"/>
<InvalidPassByReference errorLevel="error"/>
<InvalidPropertyAssignment errorLevel="error"/>
<InvalidPropertyAssignmentValue errorLevel="error"/>
<InvalidPropertyFetch errorLevel="error"/>
<InvalidReturnStatement errorLevel="error"/>
<InvalidReturnType errorLevel="error"/>
<InvalidScalarArgument errorLevel="error"/>
<InvalidScope errorLevel="error"/>
<InvalidStaticInvocation errorLevel="error"/>
<MissingConstructor errorLevel="error"/>
<MissingDependency errorLevel="error"/>
<MissingFile errorLevel="error"/>
<MixedArgument errorLevel="error"/>
<MoreSpecificImplementedParamType errorLevel="error"/>
<MoreSpecificReturnType errorLevel="error"/>
<NoValue errorLevel="error"/>
<NoInterfaceProperties errorLevel="error"/>
<NonStaticSelfCall errorLevel="error"/>
<NullableReturnStatement errorLevel="error"/>
<NullArgument errorLevel="error"/>
<NullArrayAccess errorLevel="error"/>
<NullArrayOffset errorLevel="error"/>
<NullFunctionCall errorLevel="error"/>
<NullIterator errorLevel="error"/>
<NullOperand errorLevel="error"/>
<NullPropertyAssignment errorLevel="error"/>
<NullPropertyFetch errorLevel="error"/>
<NullReference errorLevel="error"/>
<OverriddenMethodAccess errorLevel="error"/>
<OverriddenPropertyAccess errorLevel="error"/>
<ParadoxicalCondition errorLevel="error"/>
<ParentNotFound errorLevel="error"/>
<LessSpecificImplementedReturnType errorLevel="error"/>
<MissingParamType errorLevel="error"/>
<MissingClosureParamType errorLevel="error"/>
<MissingClosureReturnType errorLevel="error"/>
<MissingPropertyType errorLevel="error"/>
<UndefinedConstant errorLevel="error"/>
<AssignmentToVoid errorLevel="info"/>
<DeprecatedClass errorLevel="info"/>
<DeprecatedConstant errorLevel="info"/>
<DeprecatedTrait errorLevel="info"/>
<DocblockTypeContradiction errorLevel="info"/>
<InvalidDocblock errorLevel="info"/>
<InvalidDocblockParamName errorLevel="info"/>
<InvalidTemplateParam errorLevel="info"/>
<DeprecatedInterface errorLevel="info"/>
<DeprecatedMethod errorLevel="info"/>
<DeprecatedProperty errorLevel="info"/>
<MethodSignatureMustOmitReturnType errorLevel="info"/>
<MismatchingDocblockParamType errorLevel="info"/>
<MismatchingDocblockReturnType errorLevel="info"/>
<MissingDocblockType errorLevel="info"/>
<MissingParamType errorLevel="info"/>
<MissingTemplateParam errorLevel="info"/>
<MissingThrowsDocblock errorLevel="info"/>
<MixedArgumentTypeCoercion errorLevel="info"/>
<MixedArrayAccess errorLevel="info"/>
<MixedArrayAssignment errorLevel="info"/>
<MixedArrayOffset errorLevel="info"/>
<MixedArrayTypeCoercion errorLevel="info"/>
<MixedAssignment errorLevel="info"/>
<MixedFunctionCall errorLevel="info"/>
<MixedInferredReturnType errorLevel="info"/>
<MixedMethodCall errorLevel="info"/>
<MixedOperand errorLevel="info"/>
<MixedPropertyAssignment errorLevel="info"/>
<MixedPropertyFetch errorLevel="info"/>
<MixedPropertyTypeCoercion errorLevel="info"/>
<MixedReturnStatement errorLevel="info"/>
<MixedReturnTypeCoercion errorLevel="info"/>
<MixedStringOffsetAssignment errorLevel="info"/>
<ParamNameMismatch errorLevel="info"/>
</issueHandlers>
</psalm>