Repository: symfony/symfony
File: src/Symfony/Component/ObjectMapper/ObjectMapper.php (PHP)

Code before:
     * @param (string|callable(mixed $value, object $object): mixed) $fn
     * @param class-string|null                                      $expectedInterface
     */
    private function getCallable(string|callable $fn, ?ContainerInterface $locator = null, ?string $expectedInterface = null): callable
    {
        if (\is_callable($fn)) {
            return $fn;
        }

        if ($locator?->has($fn)) {
            return $locator->get($fn);
        }

        throw new NoSuchCallableException(\sprintf('"%s" is not a valid callable.%s', $fn, $expectedInterface ? \sprintf(' If you use a class, make sure it implements "%s".', $expectedInterface) : ''));
    }

    /**
     * @return ?\ReflectionClass<object|T>
     */
    private function getSourceReflectionClass(object $source): ?\ReflectionClass
    {
        $metadata = $this->metadataFactory->create($source);
        try {
            $refl = new \ReflectionClass($source);
        } catch (\ReflectionException $e) {
            throw new MappingException($e->getMessage(), $e->getCode(), $e);
        }

        if ($source instanceof LazyObjectInterface) {
            $source->initializeLazyObject();
        } elseif ($refl->isUninitializedLazyObject($source)) {
            $refl->initializeLazyObject($source);
        }

        if ($metadata) {
            return $refl;
        }

        foreach ($refl->getProperties() as $property) {
            if ($this->metadataFactory->create($source, $property->getName())) {
                return $refl;
            }
        }

        return null;
    }

    private function propertyIsMappable(\ReflectionClass $targetRefl, int|string $property): bool
    {
        return $targetRefl->hasProperty($property) && $targetRefl->getProperty($property)->isPublic();
    }

Review comment (suggestion):
in the locator case we can check that the callable does implement the expected interface
in the `is_callable` check we could check if its a class (hereby not a native php `function`) then it should implement the expected interface

Code after:
        return $value;
    }

    /**
     * @param (string|callable(mixed $value, object $object): mixed) $fn
     * @param class-string|null                                      $expectedInterface
     */
    private function getCallable(string|callable $fn, ?ContainerInterface $locator = null, ?string $expectedInterface = null): callable
    {
        if (\is_callable($fn)) {
            if ($expectedInterface && \is_object($fn) && !$fn instanceof $expectedInterface) {
                throw new NoSuchCallableException(\sprintf('"%s" is not a valid callable. Make sure it implements "%s".', get_debug_type($fn), $expectedInterface));
            }

            return $fn;
        }

        if ($locator?->has($fn)) {
            $callable = $locator->get($fn);

            if ($expectedInterface && !$callable instanceof $expectedInterface) {
                throw new NoSuchCallableException(\sprintf('"%s" is not a valid callable. Make sure it implements "%s".', $fn, $expectedInterface));
            }

            return $callable;
        }

        throw new NoSuchCallableException(\sprintf('"%s" is not a valid callable.', $fn));
    }

    /**
     * @return ?\ReflectionClass<object|T>
     */
    private function getSourceReflectionClass(object $source): ?\ReflectionClass
    {
        $metadata = $this->metadataFactory->create($source);
        try {
            $refl = new \ReflectionClass($source);
        } catch (\ReflectionException $e) {
            throw new MappingException($e->getMessage(), $e->getCode(), $e);
        }

        if ($source instanceof LazyObjectInterface) {
            $source->initializeLazyObject();
        } elseif ($refl->isUninitializedLazyObject($source)) {
            $refl->initializeLazyObject($source);
        }

        if ($metadata) {
            return $refl;
        }