Question: I was wondering if using `substr(md5(rand()), 0, 17);` would be safe enough for a password reset link? If I was to generate a longer string would that make it any safer? Is MD5 at all safe? Or should I do `$token = sha1(uniqid($username, true));`?

Answer: 
The use of `substr()` or `md5()` is secondary to the use of `rand()`.

The whole point of using password reset tokens is that they're unpredictable and `rand()` is known to be weak due to the underlying LCG model.

It would be a better idea to use the system's entropy source instead, e.g.:

```
$rand = openssl_random_pseudo_bytes(8); // take 8 random bytes
$token = substr(md5($rand), 0, 17);

```

It takes bytes from the system's random source, e.g. `/dev/urandom` on Linux or the corresponding system for Windows.

**Note** that if you don't have any particular size constraint you might as well choose a full `sha1()` output and take 16 random bytes.

Also, you should treat password reset tokens as if they were (temporary, time limited) passwords when you store them in your database; I would suggest to send above token to the user and then use [`password_hash()`](http://php.net/password_hash) before you write them in the database. At a later stage you check the given token (assuming it's not expired) using `password_verify()`.
I don't see a point in using substr() unless you have some sort of length constraint that makes sense. The larger the key the better. In general, it is a good idea to use hashes in their complete form. If MD5 was deemed "secure enough" trimmed down, it would already be trimmed down. The more trimming, the higher chance of a collision. 

I prefer to use GUIDs for password reset links. GUIDs are as effectively unpredictable (and secure) as an MD5 hash of a random number and they are both 128-bit values.

Make sure to use an expiration timestamp for the reset token. I usually use 24 hours.

Don't use the system clock or any derivative. You want to ensure that a hacker can't reset another user's password, while recording the timestamp, and then guess the reset token to generate a reset URL. So don't use values directly based on the system clock or anything else predictable.

You also should use a failure / max retry count on single password reset token, just like regular logins, to limit the number of attacks that are possible. If a hacker knows the userid and is trying to guess the password reset URL, you should track the number of tries against a given userid as a login attempt, and lock the account accordingly. At most the hacker gets 3 tries, then lock the account for an hour. In that case, a substr() of a MD5 is still pretty secure.