diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fe59366 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + tests: + name: PHP ${{ matrix.php }} — Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: intl, json, mbstring + coverage: none + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: vendor + key: composer-${{ matrix.php }}-${{ hashFiles('composer.json') }} + restore-keys: composer-${{ matrix.php }}- + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run PHPUnit + run: vendor/bin/phpunit --colors + + static-analysis: + name: PHPStan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: intl, json, mbstring + coverage: none + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: vendor + key: composer-8.3-${{ hashFiles('composer.json') }} + restore-keys: composer-8.3- + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run PHPStan + run: vendor/bin/phpstan analyse src tests --no-progress diff --git a/README.md b/README.md index a81a0e4..0a15879 100644 --- a/README.md +++ b/README.md @@ -65,14 +65,14 @@ Use `ValidatorFactory` to retrieve any validator by its type key: ```php use QDenka\EasyValidation\Application\Validators\ValidatorFactory; -\$types = ['email','google_email','disposable_email','url','number','date','ip','uuid','json','base64','phone']; -foreach (\$types as \$type) { - \$validator = ValidatorFactory::create(\$type); - \$value = 'test_value_for_' . \$type; - if (\$validator && \$validator->validate(\$value)) { - echo "[\$type] valid\n"; +$types = ['email','google_email','disposable_email','url','number','date','ip','uuid','json','base64','phone']; +foreach ($types as $type) { + $validator = ValidatorFactory::create($type); + $value = 'test_value_for_' . $type; + if ($validator && $validator->validate($value)) { + echo "[$type] valid\n"; } else { - echo "[\$type] invalid\n"; + echo "[$type] invalid\n"; } } ``` @@ -92,7 +92,7 @@ if (Validator::isGoogleMail('user@gmail.com')) { echo "It's a Gmail address"; } -if (!Validator::isDisposableEmail('temp@mailinator.com')) { +if (Validator::isDisposableEmail('temp@mailinator.com')) { echo "Disposable email detected"; } @@ -115,7 +115,7 @@ Validator::isValidPhone('+1234567890'); | ------------------ | ---------------------- | ------------------------------------- | | `email` | `isValidEmail()` | RFC-compliant email | | `google_email` | `isGoogleMail()` | Gmail & Googlemail domains | -| `disposable_email` | `!isDisposableEmail()` | Blacklisted disposable domains | +| `disposable_email` | `isDisposableEmail()` | Blacklisted disposable domains | | `url` | `isValidUrl()` | HTTP/HTTPS URLs | | `number` | `isValidNumber()` | Numeric values | | `date` | `isValidDate()` | Date in `Y-m-d` (configurable format) | @@ -150,8 +150,8 @@ Ensure all domains are lowercase. To add a new validator: 1. Implement `ValidatorInterface` in `src/Domain/YourType/YourValidator.php`. -2. Add your class to `VALIDATOR_MAP` in `src/Infrastructure/Factories/Validator.php`. -3. (Optional) Add a static helper in the same class. +2. Register your class in `ValidatorFactory` (`src/Application/Validators/ValidatorFactory.php`). +3. (Optional) Add a static helper in `Validator` facade (`src/Infrastructure/Factories/Validator.php`). 4. Write tests under `tests/` following existing patterns. --- diff --git a/src/Application/Validators/ValidatorFactory.php b/src/Application/Validators/ValidatorFactory.php index 861d14e..12e5f75 100644 --- a/src/Application/Validators/ValidatorFactory.php +++ b/src/Application/Validators/ValidatorFactory.php @@ -2,22 +2,25 @@ namespace QDenka\EasyValidation\Application\Validators; -use QDenka\EasyValidation\Domain\Ip\IpValidator; -use QDenka\EasyValidation\Domain\Uuid\UuidValidator; -use QDenka\EasyValidation\Domain\Json\JsonValidator; +use QDenka\EasyValidation\Domain\Contracts\ValidatorFactoryInterface; +use QDenka\EasyValidation\Domain\Contracts\ValidatorInterface; use QDenka\EasyValidation\Domain\Base64\Base64Validator; -use QDenka\EasyValidation\Domain\Phone\PhoneNumberValidator; +use QDenka\EasyValidation\Domain\Date\DateValidator; +use QDenka\EasyValidation\Domain\Email\DisposableEmailValidator; use QDenka\EasyValidation\Domain\Email\EmailValidator; use QDenka\EasyValidation\Domain\Email\GoogleMailValidator; -use QDenka\EasyValidation\Domain\Email\DisposableEmailValidator; -use QDenka\EasyValidation\Domain\Date\DateValidator; +use QDenka\EasyValidation\Domain\Ip\IpValidator; +use QDenka\EasyValidation\Domain\Json\JsonValidator; use QDenka\EasyValidation\Domain\Number\NumberValidator; +use QDenka\EasyValidation\Domain\Phone\PhoneNumberValidator; use QDenka\EasyValidation\Domain\Url\UrlValidator; +use QDenka\EasyValidation\Domain\Uuid\UuidValidator; use QDenka\EasyValidation\Infrastructure\Email\FileDisposableEmailDomainProvider; - class ValidatorFactory implements ValidatorFactoryInterface { + private const CONFIG_PATH = __DIR__ . '/../../../config/disposable_domains.php'; + public static function create(string $type): ?ValidatorInterface { switch (strtolower($type)) { @@ -26,14 +29,14 @@ public static function create(string $type): ?ValidatorInterface case 'google_email': return new GoogleMailValidator(); case 'disposable_email': - $provider = new FileDisposableEmailDomainProvider(__DIR__ . '/../../config/disposable_domains.php'); + $provider = new FileDisposableEmailDomainProvider(self::CONFIG_PATH); return new DisposableEmailValidator($provider); - case 'date': - return new DateValidator(); - case 'number': - return new NumberValidator(); case 'url': return new UrlValidator(); + case 'number': + return new NumberValidator(); + case 'date': + return new DateValidator(); case 'ip': return new IpValidator(); case 'uuid': diff --git a/src/Application/Validators/ValidatorFactoryInterface.php b/src/Application/Validators/ValidatorFactoryInterface.php deleted file mode 100644 index 54acbbf..0000000 --- a/src/Application/Validators/ValidatorFactoryInterface.php +++ /dev/null @@ -1,17 +0,0 @@ - EmailValidator::class, - 'google_email' => GoogleMailValidator::class, - 'disposable_email' => DisposableEmailValidator::class, - 'url' => UrlValidator::class, - 'number' => NumberValidator::class, - 'date' => DateValidator::class, - 'ip' => IpValidator::class, - 'uuid' => UuidValidator::class, - 'json' => JsonValidator::class, - 'base64' => Base64Validator::class, - 'phone' => PhoneNumberValidator::class, - ]; - public static function isValidEmail(string $value): bool { return self::validate('email', $value); @@ -55,7 +29,7 @@ public static function isGoogleMail(string $value): bool */ public static function isDisposableEmail(string $value): bool { - return ! self::validate('disposable_email', $value); + return !self::validate('disposable_email', $value); } public static function isValidUrl(string $value): bool @@ -99,36 +73,20 @@ public static function isValidPhone(string $value): bool } /** - * Generic validation by type. - * - * @param string $type - * @param string $value - * @return bool + * Generic validation by type key. */ public static function validate(string $type, string $value): bool { - $validator = self::create($type); + $validator = ValidatorFactory::create($type); - return $validator && $validator->validate($value); + return $validator !== null && $validator->validate($value); } /** - * @inheritdoc + * Proxy to ValidatorFactory::create(). */ public static function create(string $type): ?ValidatorInterface { - if (! array_key_exists($type, self::VALIDATOR_MAP)) { - return null; - } - - $class = self::VALIDATOR_MAP[$type]; - - if ($type === 'disposable_email') { - $configPath = __DIR__ . '/../../../config/disposable_domains.php'; - $provider = new FileDisposableEmailDomainProvider($configPath); - return new $class($provider); - } - - return new $class(); + return ValidatorFactory::create($type); } } diff --git a/tests/Infrastructure/ValidatorTest.php b/tests/Infrastructure/ValidatorTest.php index 1caedca..3b1ae12 100644 --- a/tests/Infrastructure/ValidatorTest.php +++ b/tests/Infrastructure/ValidatorTest.php @@ -3,53 +3,218 @@ namespace QDenka\EasyValidation\Tests\Infrastructure; use PHPUnit\Framework\TestCase; -use QDenka\EasyValidation\Application\Validators\ValidatorFactoryInterface; use QDenka\EasyValidation\Infrastructure\Factories\Validator; class ValidatorTest extends TestCase { - public function testIsValidEmailValid() + // --- Email --- + + public function testIsValidEmailValid(): void { $this->assertTrue(Validator::isValidEmail('test@example.com')); } - public function testIsValidEmailInvalid() + public function testIsValidEmailInvalid(): void { $this->assertFalse(Validator::isValidEmail('invalid_email')); } - public function testIsValidUrlValid() + // --- Google Mail --- + + public function testIsGoogleMailValid(): void + { + $this->assertTrue(Validator::isGoogleMail('user@gmail.com')); + } + + public function testIsGoogleMailValidGooglemail(): void + { + $this->assertTrue(Validator::isGoogleMail('user@googlemail.com')); + } + + public function testIsGoogleMailInvalidDomain(): void + { + $this->assertFalse(Validator::isGoogleMail('user@yahoo.com')); + } + + public function testIsGoogleMailInvalidFormat(): void + { + $this->assertFalse(Validator::isGoogleMail('not-an-email')); + } + + // --- Disposable Email --- + + public function testIsDisposableEmailDetectsDisposable(): void + { + $this->assertTrue(Validator::isDisposableEmail('user@mailinator.com')); + } + + public function testIsDisposableEmailAllowsNormal(): void + { + $this->assertFalse(Validator::isDisposableEmail('user@example.com')); + } + + // --- URL --- + + public function testIsValidUrlValidHttp(): void { $this->assertTrue(Validator::isValidUrl('http://www.example.com')); } - public function testIsValidUrlInvalid() + public function testIsValidUrlValidHttps(): void + { + $this->assertTrue(Validator::isValidUrl('https://example.com/path?q=1')); + } + + public function testIsValidUrlInvalid(): void { $this->assertFalse(Validator::isValidUrl('invalid_url')); } - public function testIsValidNumberValid() + public function testIsValidUrlRejectsFtp(): void + { + $this->assertFalse(Validator::isValidUrl('ftp://files.example.com')); + } + + // --- Number --- + + public function testIsValidNumberInteger(): void { $this->assertTrue(Validator::isValidNumber('123')); } - public function testIsValidNumberInvalid() + public function testIsValidNumberFloat(): void + { + $this->assertTrue(Validator::isValidNumber('123.45')); + } + + public function testIsValidNumberNegative(): void + { + $this->assertTrue(Validator::isValidNumber('-42')); + } + + public function testIsValidNumberInvalid(): void { - $this->assertFalse(Validator::isValidNumber('invalid_number')); + $this->assertFalse(Validator::isValidNumber('abc')); } - public function testIsValidDateValid() + // --- Date --- + + public function testIsValidDateValid(): void { $this->assertTrue(Validator::isValidDate('2023-04-23')); } - public function testIsValidDateInvalid() + public function testIsValidDateInvalid(): void { $this->assertFalse(Validator::isValidDate('invalid_date')); } - public function testCreateInvalidValidator() + public function testIsValidDateRejectsImpossible(): void + { + $this->assertFalse(Validator::isValidDate('2023-02-30')); + } + + // --- IP --- + + public function testIsValidIpV4(): void + { + $this->assertTrue(Validator::isValidIp('192.168.1.1')); + } + + public function testIsValidIpV6(): void + { + $this->assertTrue(Validator::isValidIp('2001:db8::1')); + } + + public function testIsValidIpInvalid(): void + { + $this->assertFalse(Validator::isValidIp('999.999.999.999')); + } + + public function testIsValidIpInvalidString(): void + { + $this->assertFalse(Validator::isValidIp('not-an-ip')); + } + + // --- UUID --- + + public function testIsValidUuidV4(): void + { + $this->assertTrue(Validator::isValidUuid('550e8400-e29b-41d4-a716-446655440000')); + } + + public function testIsValidUuidInvalid(): void + { + $this->assertFalse(Validator::isValidUuid('not-a-uuid')); + } + + public function testIsValidUuidInvalidVersion(): void + { + $this->assertFalse(Validator::isValidUuid('550e8400-e29b-61d4-a716-446655440000')); + } + + // --- JSON --- + + public function testIsValidJsonObject(): void + { + $this->assertTrue(Validator::isValidJson('{"foo":1}')); + } + + public function testIsValidJsonArray(): void + { + $this->assertTrue(Validator::isValidJson('[1,2,3]')); + } + + public function testIsValidJsonInvalid(): void + { + $this->assertFalse(Validator::isValidJson('{invalid}')); + } + + // --- Base64 --- + + public function testIsValidBase64Valid(): void + { + $this->assertTrue(Validator::isValidBase64(base64_encode('hello world'))); + } + + public function testIsValidBase64Invalid(): void + { + $this->assertFalse(Validator::isValidBase64('not base64!!!')); + } + + // --- Phone --- + + public function testIsValidPhoneValid(): void + { + $this->assertTrue(Validator::isValidPhone('+1234567890')); + } + + public function testIsValidPhoneValidLong(): void + { + $this->assertTrue(Validator::isValidPhone('+442071234567')); + } + + public function testIsValidPhoneInvalidNoPlus(): void + { + $this->assertFalse(Validator::isValidPhone('1234567890')); + } + + public function testIsValidPhoneInvalidTooShort(): void + { + $this->assertFalse(Validator::isValidPhone('+123')); + } + + // --- Factory --- + + public function testCreateReturnsNullForUnknownType(): void { $this->assertNull(Validator::create('invalid_type')); } + + public function testCreateReturnsValidatorForKnownType(): void + { + $validator = Validator::create('email'); + $this->assertNotNull($validator); + $this->assertTrue($validator->validate('test@example.com')); + } }