*/ final class OppositeExpectation { /** * Creates a new opposite expectation. * * @param Expectation $original */ public function __construct(private readonly Expectation $original) {} /** * Asserts that the value array not has the provided $keys. * * @param array> $keys * @return Expectation */ public function toHaveKeys(array $keys): Expectation { foreach ($keys as $k => $key) { try { if (is_array($key)) { $this->toHaveKeys(array_keys(Arr::dot($key, $k.'.'))); } else { $this->original->toHaveKey($key); } } catch (ExpectationFailedException) { continue; } $this->throwExpectationFailedException('toHaveKey', [$key]); } return $this->original; } /** * Asserts that the given expectation target does not use any of the given dependencies. * * @param array|string $targets */ public function toUse(array|string $targets): ArchExpectation { return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): SingleArchExpectation => ToUse::make($this->original, $target)->opposite( fn () => $this->throwExpectationFailedException('toUse', $target), ), is_string($targets) ? [$targets] : $targets)); } /** * Asserts that the given expectation target does not have the given permissions */ public function toHaveFileSystemPermissions(string $permissions): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) !== $permissions, sprintf('permissions not to be [%s]', $permissions), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || array_filter( Reflection::getMethodsFromReflectionClass($object->reflectionClass), fn (ReflectionMethod $method): bool => (enum_exists($object->name) === false || in_array($method->name, ['from', 'tryFrom', 'cases'], true) === false) && realpath($method->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line && $method->getDocComment() !== false, ) === [], 'to have methods without documentation / annotations', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); } /** * Not supported. */ public function toHavePropertiesDocumented(): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || array_filter( Reflection::getPropertiesFromReflectionClass($object->reflectionClass), fn (ReflectionProperty $property): bool => (enum_exists($object->name) === false || in_array($property->name, ['value', 'name'], true) === false) && realpath($property->getDeclaringClass()->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line && $property->isPromoted() === false && $property->getDocComment() !== false, ) === [], 'to have properties without documentation / annotations', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); } /** * Asserts that the given expectation target does not use the "declare(strict_types=1)" declaration. */ public function toUseStrictTypes(): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'), 'not to use strict types', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'original, fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isFinal(), 'not to be final', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target is not readonly. */ public function toBeReadonly(): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line 'not to be readonly', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target is not trait. */ public function toBeTrait(): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isTrait(), 'not to be trait', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are not traits. */ public function toBeTraits(): ArchExpectation { return $this->toBeTrait(); } /** * Asserts that the given expectation target is not abstract. */ public function toBeAbstract(): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isAbstract(), 'not to be abstract', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target does not have a specific method. */ public function toHaveMethod(string $method): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod($method), 'to not have method', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target not to have the public methods besides the given methods. * * @param array|string $methods */ public function toHavePublicMethodsBesides(array|string $methods): ArchExpectation { $methods = is_array($methods) ? $methods : [$methods]; return Targeted::make( $this->original, function (ObjectDescription $object) use ($methods): bool { $reflectionMethods = isset($object->reflectionClass) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PUBLIC) : []; foreach ($reflectionMethods as $reflectionMethod) { if (! in_array($reflectionMethod->name, $methods, true)) { return false; } } return true; }, $methods === [] ? 'not to have public methods' : sprintf("not to have public methods besides '%s'", implode("', '", $methods)), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'public function')), ); } /** * Asserts that the given expectation target not to have the protected methods besides the given methods. * * @param array|string $methods */ public function toHaveProtectedMethodsBesides(array|string $methods): ArchExpectation { $methods = is_array($methods) ? $methods : [$methods]; return Targeted::make( $this->original, function (ObjectDescription $object) use ($methods): bool { $reflectionMethods = isset($object->reflectionClass) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PROTECTED) : []; foreach ($reflectionMethods as $reflectionMethod) { if (! in_array($reflectionMethod->name, $methods, true)) { return false; } } return true; }, $methods === [] ? 'not to have protected methods' : sprintf("not to have protected methods besides '%s'", implode("', '", $methods)), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'protected function')), ); } /** * Asserts that the given expectation target not to have the private methods besides the given methods. * * @param array|string $methods */ public function toHavePrivateMethodsBesides(array|string $methods): ArchExpectation { $methods = is_array($methods) ? $methods : [$methods]; return Targeted::make( $this->original, function (ObjectDescription $object) use ($methods): bool { $reflectionMethods = isset($object->reflectionClass) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PRIVATE) : []; foreach ($reflectionMethods as $reflectionMethod) { if (! in_array($reflectionMethod->name, $methods, true)) { return false; } } return true; }, $methods === [] ? 'not to have private methods' : sprintf("not to have private methods besides '%s'", implode("', '", $methods)), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'private function')), ); } /** * Asserts that the given expectation target is not enum. */ public function toBeEnum(): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum(), 'not to be enum', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are not enums. */ public function toBeEnums(): ArchExpectation { return $this->toBeEnum(); } /** * Asserts that the given expectation targets is not class. */ public function toBeClass(): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! class_exists($object->name), 'not to be class', FileLineFinder::where(fn (string $line): bool => true), ); } /** * Asserts that the given expectation targets are not classes. */ public function toBeClasses(): ArchExpectation { return $this->toBeClass(); } /** * Asserts that the given expectation target is not interface. */ public function toBeInterface(): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isInterface(), 'not to be interface', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are not interfaces. */ public function toBeInterfaces(): ArchExpectation { return $this->toBeInterface(); } /** * Asserts that the given expectation target to be not subclass of the given class. */ public function toExtend(string $class): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isSubclassOf($class), sprintf("not to extend '%s'", $class), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target to be not have any parent class. */ public function toExtendNothing(): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() !== false, 'to extend a class', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target not to use the given trait. */ public function toUseTrait(string $trait): ArchExpectation { return $this->toUseTraits($trait); } /** * Asserts that the given expectation target not to use the given traits. * * @param array|string $traits */ public function toUseTraits(array|string $traits): ArchExpectation { $traits = is_array($traits) ? $traits : [$traits]; return Targeted::make( $this->original, function (ObjectDescription $object) use ($traits): bool { foreach ($traits as $trait) { if (in_array($trait, $object->reflectionClass->getTraitNames(), true)) { return false; } } return true; }, "not to use traits '".implode("', '", $traits)."'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target not to implement the given interfaces. * * @param array|string $interfaces */ public function toImplement(array|string $interfaces): ArchExpectation { $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; return Targeted::make( $this->original, function (ObjectDescription $object) use ($interfaces): bool { foreach ($interfaces as $interface) { if ($object->reflectionClass->implementsInterface($interface)) { return false; } } return true; }, "not to implement '".implode("', '", $interfaces)."'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target to not implement any interfaces. */ public function toImplementNothing(): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() !== [], 'to implement an interface', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Not supported. */ public function toOnlyImplement(): never { throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']); } /** * Asserts that the given expectation target to not have the given prefix. */ public function toHavePrefix(string $prefix): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! str_starts_with($object->reflectionClass->getShortName(), $prefix), "not to have prefix '{$prefix}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target to not have the given suffix. */ public function toHaveSuffix(string $suffix): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! str_ends_with($object->reflectionClass->getName(), $suffix), "not to have suffix '{$suffix}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Not supported. */ public function toOnlyUse(): never { throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']); } /** * Not supported. */ public function toUseNothing(): never { throw InvalidExpectation::fromMethods(['not', 'toUseNothing']); } /** * Asserts that the given expectation dependency is not used. */ public function toBeUsed(): ArchExpectation { return ToBeUsedInNothing::make($this->original); } /** * Asserts that the given expectation dependency is not used by any of the given targets. * * @param array|string $targets */ public function toBeUsedIn(array|string $targets): ArchExpectation { return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): GroupArchExpectation => ToBeUsedIn::make($this->original, $target)->opposite( fn () => $this->throwExpectationFailedException('toBeUsedIn', $target), ), is_string($targets) ? [$targets] : $targets)); } public function toOnlyBeUsedIn(): never { throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedIn']); } /** * Asserts that the given expectation dependency is not used. */ public function toBeUsedInNothing(): never { throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']); } /** * Asserts that the given expectation dependency is not an invokable class. */ public function toBeInvokable(): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod('__invoke'), 'to not be invokable', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); } /** * Asserts that the given expectation target not to have the given attribute. */ public function toHaveAttribute(string $attribute): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) === [], "to not have attribute '{$attribute}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); } /** * Handle dynamic method calls into the original expectation. * * @param array $arguments * @return Expectation|Expectation|never */ public function __call(string $name, array $arguments): Expectation { try { if (! is_object($this->original->value) && method_exists(PendingArchExpectation::class, $name)) { throw InvalidExpectation::fromMethods(['not', $name]); } /* @phpstan-ignore-next-line */ $this->original->{$name}(...$arguments); } catch (ExpectationFailedException|AssertionFailedError) { return $this->original; } $this->throwExpectationFailedException($name, $arguments); } /** * Handle dynamic properties gets into the original expectation. * * @return Expectation|Expectation|never */ public function __get(string $name): Expectation { try { if (! is_object($this->original->value) && method_exists(PendingArchExpectation::class, $name)) { throw InvalidExpectation::fromMethods(['not', $name]); } $this->original->{$name}; // @phpstan-ignore-line } catch (ExpectationFailedException) { return $this->original; } $this->throwExpectationFailedException($name); } /** * Creates a new expectation failed exception with a nice readable message. * * @param array|string $arguments */ public function throwExpectationFailedException(string $name, array|string $arguments = []): never { $arguments = is_array($arguments) ? $arguments : [$arguments]; $exporter = Exporter::default(); $toString = fn (mixed $argument): string => $exporter->shortenedExport($argument); throw new ExpectationFailedException(sprintf( 'Expecting %s not %s %s.', $toString($this->original->value), strtolower((string) preg_replace('/(? $toString($argument), $arguments)), )); } /** * Asserts that the given expectation target does not have a constructor method. */ public function toHaveConstructor(): ArchExpectation { return $this->toHaveMethod('__construct'); } /** * Asserts that the given expectation target does not have a destructor method. */ public function toHaveDestructor(): ArchExpectation { return $this->toHaveMethod('__destruct'); } /** * Asserts that the given expectation target is not a backed enum of given type. */ private function toBeBackedEnum(string $backingType): ArchExpectation { return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum() || ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line || (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line 'not to be '.$backingType.' backed enum', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are not string backed enums. */ public function toBeStringBackedEnums(): ArchExpectation { return $this->toBeStringBackedEnum(); } /** * Asserts that the given expectation targets are not int backed enums. */ public function toBeIntBackedEnums(): ArchExpectation { return $this->toBeIntBackedEnum(); } /** * Asserts that the given expectation target is not a string backed enum. */ public function toBeStringBackedEnum(): ArchExpectation { return $this->toBeBackedEnum('string'); } /** * Asserts that the given expectation target is not an int backed enum. */ public function toBeIntBackedEnum(): ArchExpectation { return $this->toBeBackedEnum('int'); } }