Time Machine for Developers

The problem

Time Machine will backup your files. For a developer, and having loads of node_modules and vendor directories for projects, this is huge time suck and waste since those can be brought back with npm, yarn, composer, etc.

The solution

Directories can be excluded from Time Machine using the cli.

Adding exclusions

First, verify that you can get a list of directories that you want to exclude:

cd "$HOME/code" && find $(pwd) \
    -maxdepth 3 \
    -type d \( -name vendor -o -name node_modules \)

The first part of the above assumes you keep your code in a code directory in your home directory. You'll need to change that if that's not the case for you.

Executing this command should output a list of directories that have vendor or node_modules directories. These are what you want excluded from Time Machine.

Next, we can add to the above command to pipe those into the Time Machine utility tool, tmutil, to be excluded.

cd "$HOME/code" && find $(pwd) \
    -maxdepth 3 \
    -type d \( -name vendor -o -name node_modules \) \
    -prune \
    -exec tmutil addexclusion {} \; \
    -exec tmutil isexcluded {} \;

Viewing exclusions

You can see all excluded files/directories with the following command:

sudo mdfind "com_apple_backup_excludeItem = 'com.apple.backupd'"

You can see if a specific file or directory is excluded with:

tmutil isexcluded /some/file/path.txt

Verify that directories we excluded earlier are in fact exluded:

cd "$HOME/code" && find $(pwd) \
  -maxdepth 3 \
  -type d \( -name vendor -o -name node_modules \) \
  -exec tmutil isexcluded {} + | grep -F "[Excluded]"

Remove exclusions

Or remove all the exclusions we added

cd "$HOME/code" && find $(pwd) \
  -maxdepth 3 \
  -type d \( -name vendor -o -name node_modules \) \
  -prune \
  -exec tmutil removeexclusion {} \; \
  -exec tmutil isexcluded {} \;

Automate

We can automate this so if we create a new project we don't have to worry about manually adding the exclusion with a cron job.

Create a bash file that will run the addexclusion commands:

#!/usr/bin/env bash

cd "$HOME/code" && find $(pwd) \
  -maxdepth 3 \
  -type d \( -name vendor -o -name node_modules \) \
  -prune \
  -exec tmutil addexclusion {} \; \
  -exec tmutil isexcluded {} \;

Remember to change the $HOME/code to wherever you keep your projects. I put this at $HOME/code/machine-utils/time-machine-exclusions.sh.

Make sure it's executuble:

chmod +x $HOME/code/machine-utils/time-machine-exclusions.sh

Add this as a cronjob to run every hour:

  • crontab -e
  • press i to go into "insert" mode
  • paste the following line (Change according to where you saved the file on your machine. The full path is required.):
    • 0 * * * * /Users/davidadams/code/machine-utils/time-machine-exclusions.sh
  • press esc to exit "insert" mode
  • type :x to save and exit

Tags: Mac, Time Machine

Throw exceptions in repositories

Here's why you should consider throwing exceptions in your repositories.

99% of the time you're going to want to bail if you're expecting to find an entity by a specific key. Having code like the two private methods in the old code below in every single one of your handlers is crazy and not DRY.

I think the idea of "only throw exceptions when something exceptional happens" is too generic and doesn't take context into consideration.

A repository's only job is to find entities. If you give it a key and it can't find it, i think that's exceptional(in the context of the repository).

If it actually is expected in your client code it's not a big deal, catch the EntityNotFoundException and carry on.

This also unifies the types of exceptions thrown. It is entirely possible some handlers could be throwing a EntityNotFoundException while your partner developer is throwing a ValidationException. Encapsulate that in the repository.

class AddPersonCommandHandler
{
    private $categories;

    private $people;

    public function __construct(CategoryRepsoitory $categories, PersonRepository $people)
    {
        $this->categories = $categories;
        $this->people = $people;
    }

    public function handle(AddPersonCommand $command)
    {
        $category = $this->findCategory($command->getCategoryId()));
        $referrer = $this->findPerson($command->getReferrerId());

        $person = new Person(
            $command->getName(),
            $category,
            $referrer
        );

        $this->people->add($person);
    }

    private function findCategory($id)
    {
        if (null === $category = $this->categories->find($id)) {
            throw new EntityNotFoundException("Cateogry not found.");
        }

        return $category;
    }

    private function findPerson($id)
    {
        if (null === $person = $this->people->find($id)) {
            throw new EntityNotFoundException("Person not found.");
        }

        return $person;     
    }
}

Becomes:

class AddPersonCommandHandler
{
    private $categories;

    private $people;

    public function __construct(CategoryRepsoitory $categories, PersonRepository $people)
    {
        $this->categories = $categories;
        $this->people = $people;
    }

    public function handle(AddPersonCommand $command)
    {
        $category = $this->categories->find($command->getCategoryId());
        $referrer = $this->people->find($command->getReferrerId());

        $person = new Person(
            $command->getName(),
            $category,
            $referrer
        );

        $this->people->add($person);
    }
}

Tags: PHP

Protect your ArrayCollections

Here's a little Doctrine ORM tip to safeguard your business logic when dealing with collections.

Do you have some business logic tied up in your adder methods? Something like this maybe.

class Cart
{
    private $items;

    public function __construct()
    {
        $this->items = new ArrayCollection();
    }

    public function addItem(Item $item)
    {
        if ($item->isExpired()) {
            throw new ItemExpiredException(sprintf("The item is expired: %s.", $item->getName()));
        }

        $this->items->add($item);
    }

    public function getItems()
    {
        return $this->items;
    }
}

Look how easy it is for another developer to accidentally skirt your nice logic.

$cart = new Cart();
$cart->getItems()->add($expiredItem);

It's easy to prevent though!

class Cart
{
    // ...

    public function getItems()
    {
        return new ArrayCollection($this->items->toArray());
    }
}

If you're concerned with them still calling $cart->getItems()->add($expiredItem) and silently not doing what they think it should be doing, simply return $this->items->toArray() from the getItems() method.

If you want to return an ArrayCollection and also take advantage of Doctrine's extra lazy functionality one solution would be to subclass ArrayCollection and proxy read-only calls to the lazy collection.

class Cart
{
    // ...

    public function getItems()
    {
        return new ReadOnlyCollection($this->items);
    }
}

Tags: PHP, DoctrineORM