Avoiding imports and aliases

Berlin, Germany

PHP 5.3, released on June 30, 2009, introduced namespaces as well as imports and aliases.

Developers quickly adopted these features. Where previously a class was named Foo_Bar_Baz_Qux, namespaces allow calling it Foo\Bar\Baz\Qux.

Imports

With imports, I can import the fully-qualified class name and reference the class by its terminating class name:

<?php

use Foo\Bar\Baz\Qux;

$qux = new Qux();

Alternatively, I can import a namespace prefix and reference the class relative to it:

<?php

use Foo\Bar;

$qux = new Bar\Baz\Qux();

Aliases

Aliases, you guessed it, allow importing either and assigning aliases to it:

<?php

use Foo\Bar as Quux;
use Foo\Bar\Baz\Qux as Quuz;

$qux = new Quux\Baz\Qux();
$anotherQux = new Quuz();

Problems

When it comes to the possibilities of how I can use these features, there are two extremes. At one end, I use fully-qualified class names everywhere. At the other end, I import every single class.

Using fully-qualified class names everywhere is a bit like writing code as if today is June 29, 2009. It does not make any sense - so let us not get into it.

Importing every single class is possible, but potentially leads to a long list of imports. A long list of imports takes a long time to read. For every class I add to this list, developers need to recall the context from which I introduced it. Every time I import a class, I risk conflicts.

Aliases can help to avoid these conflicts. However, aliases only make the problem worse. For every alias, developers need to translate between the original class name and the aliased class name.

With aliases, navigating the code becomes more difficult. In PhpStorm, clicking on a symbol navigates to where it was declared. When I click on a class name referenced in code, I navigate to the class declaration. However, when I click on an alias, I navigate to the alias declaration. I now need to click again on the class name to navigate to its declaration.

Ugh.

Solution

The worst examples of importitis and aliasitis I have seen in a closed-source project included a class with 58 imports, and another class with 12 aliases. If we ignore for a moment that these classes were suffering from other problems, all of these issues could have been avoided by only importing a suitable parent namespace.

Here are few examples.

Take a look at the UserController from the symfony/demo application:

<?php

namespace App\Controller;

use App\Entity\Comment;
use App\Entity\Post;
use App\Events\CommentCreatedEvent;
use App\Form\CommentType;
use App\Repository\PostRepository;
use App\Repository\TagRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

It has a fairly long list of imports, that can easily be reduced by importing only parent namespaces:

diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php
index 85228f0..82723de 100644
--- a/src/Controller/UserController.php
+++ b/src/Controller/UserController.php
@@ -2,29 +2,31 @@

 namespace App\Controller;

-use App\Form\Type\ChangePasswordType;
-use App\Form\UserType;
-use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
-use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\Routing\Annotation\Route;
-use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
+use App\Form;
+use Sensio\Bundle\FrameworkExtraBundle;
+use Symfony\Bundle\FrameworkBundle;
+use Symfony\Component\HttpFoundation;
+use Symfony\Component\Routing;
+use Symfony\Component\Security;

 /**
- * @Route("/profile")
- * @IsGranted("ROLE_USER")
+ * @Routing\Annotation\Route("/profile")
+ * @FrameworkExtraBundle\Configuration\IsGranted("ROLE_USER")
  */
-class UserController extends AbstractController
+class UserController extends FrameworkBundle\Controller\AbstractController
 {
     /**
-     * @Route("/edit", methods="GET|POST", name="user_edit")
+     * @Routing\Annotation\Route(
+     *     "/edit",
+     *     methods="GET|POST",
+     *     name="user_edit"
+     * )
      */
-    public function edit(Request $request): Response
+    public function edit(HttpFoundation\Request $request): HttpFoundation\Response
     {
         $user = $this->getUser();

-        $form = $this->createForm(UserType::class, $user);
+        $form = $this->createForm(Form\UserType::class, $user);
         $form->handleRequest($request);

         if ($form->isSubmitted() && $form->isValid()) {
@@ -42,13 +44,19 @@ class UserController extends AbstractController
     }

     /**
-     * @Route("/change-password", methods="GET|POST", name="user_change_password")
+     * @Routing\Annotation\Route(
+     *     "/change-password",
+     *     methods="GET|POST",
+     *     name="user_change_password"
+     * )
      */
-    public function changePassword(Request $request, UserPasswordEncoderInterface $encoder): Response
-    {
+    public function changePassword(
+        HttpFoundation\Request $request,
+        Security\Core\Encoder\UserPasswordEncoderInterface $encoder
+    ): HttpFoundation\Response {
         $user = $this->getUser();

-        $form = $this->createForm(ChangePasswordType::class);
+        $form = $this->createForm(Form\Type\ChangePasswordType::class);
         $form->handleRequest($request);

         if ($form->isSubmitted() && $form->isValid()) {

We have shortened the list of imports and referenced classes relative to the imported namespace prefixes. We can see immediately the context from which we imported a class. Additionally, we wrapped a few lines. Cognitive load is reduced.

Take a look at the Post entity from the same application:

<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;

The list of imports is not so long, but unnecessary aliases are used, and these can be avoided as well:

diff --git a/src/Entity/Post.php b/src/Entity/Post.php
index a47a862..a2328fd 100644
--- a/src/Entity/Post.php
+++ b/src/Entity/Post.php
@@ -11,16 +11,19 @@

 namespace App\Entity;

-use Doctrine\Common\Collections\ArrayCollection;
-use Doctrine\Common\Collections\Collection;
-use Doctrine\ORM\Mapping as ORM;
-use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
-use Symfony\Component\Validator\Constraints as Assert;
+use Doctrine\Common;
+use Doctrine\ORM;
+use Symfony\Bridge\Doctrine;
+use Symfony\Component\Validator;

 /**
- * @ORM\Entity(repositoryClass="App\Repository\PostRepository")
- * @ORM\Table(name="symfony_demo_post")
- * @UniqueEntity(fields={"slug"}, errorPath="title", message="post.slug_unique")
+ * @ORM\Mapping\Entity(repositoryClass="App\Repository\PostRepository")
+ * @ORM\Mapping\Table(name="symfony_demo_post")
+ * @Doctrine\Validator\Constraints\UniqueEntity(
+ *     fields={"slug"},
+ *     errorPath="title",
+ *     message="post.slug_unique"
+ * )
  */
 class Post
 {
@@ -35,88 +38,97 @@ class Post
     /**
      * @var int
      *
-     * @ORM\Id
-     * @ORM\GeneratedValue
-     * @ORM\Column(type="integer")
+     * @ORM\Mapping\Id
+     * @ORM\Mapping\GeneratedValue
+     * @ORM\Mapping\Column(type="integer")
      */
     private $id;

     /**
      * @var string
      *
-     * @ORM\Column(type="string")
-     * @Assert\NotBlank
+     * @ORM\Mapping\Column(type="string")
+     * @Validator\Constraints\NotBlank
      */
     private $title;

     /**
      * @var string
      *
-     * @ORM\Column(type="string")
+     * @ORM\Mapping\Column(type="string")
      */
     private $slug;

     /**
      * @var string
      *
-     * @ORM\Column(type="string")
-     * @Assert\NotBlank(message="post.blank_summary")
-     * @Assert\Length(max=255)
+     * @ORM\Mapping\Column(type="string")
+     * @Validator\Constraints\NotBlank(message="post.blank_summary")
+     * @Validator\Constraints\Length(max=255)
      */
     private $summary;

     /**
      * @var string
      *
-     * @ORM\Column(type="text")
-     * @Assert\NotBlank(message="post.blank_content")
-     * @Assert\Length(min=10, minMessage="post.too_short_content")
+     * @ORM\Mapping\Column(type="text")
+     * @Validator\Constraints\NotBlank(message="post.blank_content")
+     * @Validator\Constraints\Length(
+     *     min=10,
+     *     minMessage="post.too_short_content"
+     * )
      */
     private $content;

     /**
      * @var \DateTime
      *
-     * @ORM\Column(type="datetime")
+     * @ORM\Mapping\Column(type="datetime")
      */
     private $publishedAt;

     /**
      * @var User
      *
-     * @ORM\ManyToOne(targetEntity="App\Entity\User")
-     * @ORM\JoinColumn(nullable=false)
+     * @ORM\Mapping\ManyToOne(targetEntity="App\Entity\User")
+     * @ORM\Mapping\JoinColumn(nullable=false)
      */
     private $author;

     /**
-     * @var Comment[]|ArrayCollection
+     * @var Comment[]|Common\Collections\ArrayCollection
      *
-     * @ORM\OneToMany(
+     * @ORM\Mapping\OneToMany(
      *      targetEntity="Comment",
      *      mappedBy="post",
      *      orphanRemoval=true,
      *      cascade={"persist"}
      * )
-     * @ORM\OrderBy({"publishedAt": "DESC"})
+     * @ORM\Mapping\OrderBy({"publishedAt": "DESC"})
      */
     private $comments;

     /**
-     * @var Tag[]|ArrayCollection
+     * @var Tag[]|Common\Collections\ArrayCollection
      *
-     * @ORM\ManyToMany(targetEntity="App\Entity\Tag", cascade={"persist"})
-     * @ORM\JoinTable(name="symfony_demo_post_tag")
-     * @ORM\OrderBy({"name": "ASC"})
-     * @Assert\Count(max="4", maxMessage="post.too_many_tags")
+     * @ORM\Mapping\ManyToMany(
+     *     targetEntity="App\Entity\Tag",
+     *     cascade={"persist"}
+     * )
+     * @ORM\Mapping\JoinTable(name="symfony_demo_post_tag")
+     * @ORM\Mapping\OrderBy({"name": "ASC"})
+     * @Validator\Constraints\Count(
+     *     max="4",
+     *     maxMessage="post.too_many_tags"
+     * )
      */
     private $tags;

     public function __construct()
     {
         $this->publishedAt = new \DateTime();
-        $this->comments = new ArrayCollection();
-        $this->tags = new ArrayCollection();
+        $this->comments = new Common\Collections\ArrayCollection();
+        $this->tags = new Common\Collections\ArrayCollection();
     }

     public function getId(): ?int
@@ -174,7 +186,7 @@ class Post
         $this->author = $author;
     }

-    public function getComments(): Collection
+    public function getComments(): Common\Collections\Collection
     {
         return $this->comments;
     }
@@ -216,7 +228,7 @@ class Post
         $this->tags->removeElement($tag);
     }

-    public function getTags(): Collection
+    public function getTags(): Common\Collections\Collection
     {
         return $this->tags;
     }

Again, we have shortened the list of imports and referenced classes relative to the imported namespace prefixes. We can see immediately the context from which we imported a class. We removed unnecessary aliases Additionally, we wrapped a few lines. Cognitive load is reduced again.

Best practices

The changes I propose here do not conform with the examples found in the documentation for Doctrine or Symfony, which might be a problem when you intend to contribute to the core of Doctrine or Symfony or related projects.

When you work on other projects, you are free to do whatever you want.

Decide for yourself. Find out what works best for you.