Psalm: Support Doctrine via stubbed templates

Created on 29 Jun 2018  路  11Comments  路  Source: vimeo/psalm

<?php
namespace Doctrine\ORM {
    class EntityManager {
        /**
         * @template T
         * @template-typeof T $class_name
         * @return EntityRepository<T>
         */
        public function getRepository(string $class_name) {}
    }

    /**
     * @template T as object
     */
    class EntityRepository {
        /**
         * Finds an entity by its primary key / identifier.
         *
         * @param mixed    $id          The identifier.
         * @param int|null $lockMode    One of the \Doctrine\DBAL\LockMode::* constants
         *                              or NULL if no specific lock mode should be used
         *                              during the search.
         * @param int|null $lockVersion The lock version.
         *
         * @return T|null The entity instance or NULL if the entity can not be found.
         */
        public function find($id, $lockMode = null, $lockVersion = null)
        {
            return $this->_em->find($this->_entityName, $id, $lockMode, $lockVersion);
        }

        /**
         * Finds all entities in the repository.
         *
         * @return T[] The entities.
         */
        public function findAll()
        {
            return $this->findBy([]);
        }

        /**
         * Finds entities by a set of criteria.
         *
         * @param array      $criteria
         * @param array|null $orderBy
         * @param int|null   $limit
         * @param int|null   $offset
         *
         * @return T[] The objects.
         */
        public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
        {
            $persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);

            return $persister->loadAll($criteria, $orderBy, $limit, $offset);
        }

        /**
         * Finds a single entity by a set of criteria.
         *
         * @param array      $criteria
         * @param array|null $orderBy
         *
         * @return T|null The entity instance or NULL if the entity can not be found.
         */
        public function findOneBy(array $criteria, array $orderBy = null)
        {
            $persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);

            return $persister->load($criteria, null, null, [], null, 1, $orderBy);
        }
    }
}

namespace Doctrine\Common\Collections {
    /**
     * @template T
     */
    interface Collection extends Countable, IteratorAggregate, ArrayAccess
    {
        /**
         * Checks whether an element is contained in the collection.
         * This is an O(n) operation, where n is the size of the collection.
         *
         * @param T $element The element to search for.
         *
         * @return bool TRUE if the collection contains the element, FALSE otherwise.
         */
        public function contains($element);

        /**
         * Removes the element at the specified index from the collection.
         *
         * @param string|int $key The kex/index of the element to remove.
         *
         * @return T|null The removed element or NULL, if the collection did not contain the element.
         */
        public function remove($key);

        /**
         * Removes the specified element from the collection, if it is found.
         *
         * @param T $element The element to remove.
         *
         * @return bool TRUE if this collection contained the specified element, FALSE otherwise.
         */
        public function removeElement($element);

        /**
         * Gets the element at the specified key/index.
         *
         * @param string|int $key The key/index of the element to retrieve.
         *
         * @return T
         */
        public function get($key);

        /**
         * Gets all values of the collection.
         *
         * @return T[] The values of all elements in the collection, in the order they
         *               appear in the collection.
         */
        public function getValues();

        /**
         * Sets an element in the collection at the specified key/index.
         *
         * @param string|int $key   The key/index of the element to set.
         * @param T      $value The element to set.
         *
         * @return void
         */
        public function set($key, $value);

        /**
         * Gets a native PHP array representation of the collection.
         *
         * @return T[]
         */
        public function toArray();

        /**
         * Sets the internal iterator to the first element in the collection and returns this element.
         *
         * @return T
         */
        public function first();

        /**
         * Sets the internal iterator to the last element in the collection and returns this element.
         *
         * @return T
         */
        public function last();

        /**
         * Gets the element of the collection at the current iterator position.
         *
         * @return T
         */
        public function current();

        /**
         * Moves the internal iterator position to the next element and returns this element.
         *
         * @return T
         */
        public function next();

        /**
         * Partitions this collection in two collections according to a predicate.
         * Keys are preserved in the resulting collections.
         *
         * @param Closure $p The predicate on which to partition.
         *
         * @return Collection<T>[] An array with two elements. The first element contains the collection
         *                      of elements where the predicate returned TRUE, the second element
         *                      contains the collection of elements where the predicate returned FALSE.
         */
        public function partition(Closure $p);

        /**
         * Gets the index/key of a given element. The comparison of two elements is strict,
         * that means not only the value but also the type must match.
         * For objects this means reference equality.
         *
         * @param T $element The element to search for.
         *
         * @return int|string|bool The key/index of the element or FALSE if the element was not found.
         */
        public function indexOf($element);

        /**
         * Extracts a slice of $length elements starting at position $offset from the Collection.
         *
         * If $length is null it returns all elements from $offset to the end of the Collection.
         * Keys have to be preserved by this method. Calling this method will only return the
         * selected slice and NOT change the elements contained in the collection slice is called on.
         *
         * @param int      $offset The offset to start from.
         * @param int|null $length The maximum number of elements to return, or null for no limit.
         *
         * @return T[]
         */
        public function slice($offset, $length = null);
    }
}

Partial stubs like this should be importable into a given project by including the package via composer.

enhancement

Most helpful comment

Mostly. I don't have EntityManager there, but considered adding that (and renaming to psalm-doctrine). I plan to get back to it the week after next, when I'm back in the office.

All 11 comments

That would be immensely useful, especially being able to partially stub things. We have some stubs for Doctrine Collections and a couple of other libraries, it would be nice to have a way to distribute them.

I've updated the stubs above to one that I've got working. Unfortunately I don't have a codebase that uses Doctrine ORM to test against - any suggestions?

Also @weirdan if you could post those stubs to a new repo, you could require that repo via composer and include a ref to the vendor/.../stubs.php file in your psalm config.

I think the best thing is to require each extension repo have a stubs.php file in the root directory (which itself can require other files)

For me, relying on the file paths is a problematic part (due to the particular setup we use, but also in general), so I'd like to relegate as much of the file structure heavy-lifting to composer. Which means I'd like to see plugins addressed by namespace/classname, not by file path.

If you're open to this idea I can draft up a kind of proposal the following week.

@weirdan absolutely. The only thing I was thinking is that it I'd want to provide an easy way to disable extensions/plugins, which is cumbersome if you have to do it via Composer.

I was thinking something like

<extensions>
    <directory name="vendor/muglug/psalm-doctrine-ext" />
</extensions>

You may also composer require a project that itself composer requires a given Psalm extension in composer, but you don't want Psalm to use it when analysing your _own_ code.

The only thing I was thinking is that it I'd want to provide an easy way to disable extensions/plugins,

Sure, it's on my todo.

I was thinking something like <directory name="vendor/...."/>

That's file path, and that's what I'd like to avoid. The approach I'm working on wouldn't require any file paths (but PoC would be a bit tied to composer infrastructure, with an option to unbind it later).

You may also composer require a project that itself composer requires a given Psalm extension in composer, but you don't want Psalm to use it when analysing your own code.

It's likely to be dev-required (rather than required), and composer does not install dev-dependencies of your dependencies. Still, it's a valid point, and would be addressed.

Still, it's a valid point, and would be addressed.

Brilliant! If you can address that, then I'm happy relying on composer. I was thinking that each package would add a registration script into autoload_files, but that'd have to be a short easy snippet.

@weirdan is this done in your own plugin?

Mostly. I don't have EntityManager there, but considered adding that (and renaming to psalm-doctrine). I plan to get back to it the week after next, when I'm back in the office.

Thanks!

Was this page helpful?
0 / 5 - 0 ratings