When PHP code mixes up encode, hash, and encrypt, the bug usually shows up somewhere less glamorous than the architecture diagram: a broken login, a weird redirect, or a log file that now knows too much.
By Theo Marlowe · Published July 5, 2026
If you are deciding between encoding, hashing, and encryption, the useful questions are simple: Do I need the original value back later? Do I only need to compare values? Or do I just need the data to survive a text boundary like a URL, header, or HTML page? I start there because the function name is not the decision; the data flow is. For a quick grounding in the PHP side, the official password_hash() manual and the OWASP Password Storage Cheat Sheet are the two references I reach for when a team wants to skip the folklore and get back to work.
This cheatsheet is written for PHP developers and small product teams, so I will keep the language practical. The site’s home page, blog, and Sources/Functions… section are useful companions if you want to keep the workflow and reference material in one place.
What you will get here is a quick decision rule, a use-case map for passwords, tokens, IDs, URLs, and logs, a few PHP examples that use the built-ins correctly, and a short checklist you can run before a bad assumption ships. The punchline is boring but effective: encoding changes form, hashing checks or fingerprints, and encryption protects confidentiality when you need the original back later.

What Each Term Is For, In Plain Language
Most of the confusion comes from treating these words as if they were interchangeable. They are not. They sit in different parts of the workflow and answer different questions. If you keep that separation in mind, the choice gets much less dramatic and much more useful.
| Method | Plain-language job | Reversible? | Typical PHP examples | What it is not |
|---|---|---|---|---|
| Encoding | Change the representation so data can move through a text boundary or fit a target format. | Yes | base64_encode(), rawurlencode(), http_build_query() |
Not secrecy, not a trust boundary, not a checksum. |
| Hashing | Produce a one-way fingerprint for comparison, verification, or password storage with the right tool. | No | hash(), hash_hmac(), password_hash() |
Not reversible, not transport formatting, not encryption. |
| Encryption | Protect confidentiality so trusted code can recover the original later with the right key. | Yes, with the key | openssl_encrypt(), sodium_crypto_secretbox() |
Not a replacement for key management, integrity design, or access control. |
I use one short mental model here: encoding is for travel, hashing is for comparison, and encryption is for storage or transfer when the original value still has to come back. That is the whole game. The rest is implementation detail, which is where the bugs like to live.
Encoding deserves one extra sentence because developers often overextend it. Base64 is the classic example: it turns binary data into text-friendly characters, which is useful, but it does not make the content secret. If you can decode it without a key, it is still just representation, wearing a better shirt.
Hashing is the opposite kind of promise. A hash lets you compare values without keeping the original value in plain form. It is very good at that job. It is very bad at pretending to be a vault. In password handling, that distinction matters enough to be a policy, not a style preference.
Encryption is the only one in this trio that exists because the original must be recoverable later. That is where the key, nonce, IV, or tag suddenly become more important than the algorithm label you put in the ticket. The label is not the system. The system is the system.
The Fastest Decision Rules, In 5 Questions
When I am staring at a new value in a PHP app, I do not start with a function name. I start with five questions. The order matters because the first one that gets a strong yes usually tells you what class of tool belongs there.
- Will I need the original value back later?
If yes, you are in encryption territory. If no, keep going. - Am I only trying to compare or fingerprint the value?
If yes, hashing fits better than encryption. If this is a password, usepassword_hash(), not a generic hash. - Do I only need to move the value through a URL, HTML page, header, or other text boundary?
If yes, you want encoding or output escaping, not cryptography. - Could the value appear in logs, analytics, or reporting tools?
If yes, think about redaction, hashing, or omission before you think about convenience. - Will another developer inherit this code in six months?
If yes, choose the method that makes the intent obvious, because future-you will not remember the brilliant shortcut you made on a Tuesday.
Here is the same decision tree in a shorter form:
- Need confidentiality and later recovery? Use encryption.
- Need a one-way comparison or verification step? Use hashing.
- Need text-safe representation for transport or rendering? Use encoding.
- Need password storage? Use password hashing, not encryption and not MD5.
If the value is traveling through a reporting or referrer trail, check the Reports & Tracking area and the RedKernel Referer Tracker page to understand what the application actually emitted before you decide how to transform it. Observation first. Ceremony later.
Use-Case Mapping: Tokens, Passwords, IDs, URLs, and Logs
This is the part most developers want first, which is understandable. The catch is that the answer changes depending on what the value is supposed to do. A token is not a password, a URL parameter is not a secret, and a log line is not a safe place to be sentimental.
| Use case | Right choice | Good PHP tools | What to avoid |
|---|---|---|---|
| Passwords | Hash, using the dedicated password API. | password_hash(), password_verify() |
MD5, Base64, encryption for normal password storage. |
| Opaque API or reset tokens | Usually generate random bytes, encode for transport, and store a verifiable form. | random_bytes() plus bin2hex() or Base64url-style encoding; hash the stored copy if you only need verification. |
Using a guessable string, leaking the token into logs, or treating Base64 as secrecy. |
| Numeric or internal IDs | Encode only if you need URL-safe transport or a presentation format. | rawurlencode(), http_build_query() |
Encrypting an ID just because it feels fancier than it is. |
| URLs and query parameters | Encode the component, not the whole structured URL. | http_build_query(), rawurlencode() |
Encoding the full URL twice and then asking the browser why it looks annoyed. |
| Logs and reports | Redact first; hash only if you need stable matching or correlation. | hash_hmac() for consistent fingerprints; selective truncation or masking for display. |
Storing secrets because the log is “internal” or “temporary”. |
A practical rule for tokens is worth spelling out. If you need to compare a token later, hash the stored value and compare against the hash. If you need to show the token to a user or send it in a link, encode it so it survives transport, but do not confuse that with protection. And if you need to recover the original later, that is encryption, not a token-design shortcut.
For passwords, the correct answer is even less glamorous and more important: use password_hash(). That function exists because password storage is a special case, and special cases deserve special tools. The OWASP guidance linked above explains why the password hash should be slow enough to be annoying to attackers, not convenient enough for your application to accidentally impersonate a speed contest.
For URLs, the key distinction is component-level handling. A query parameter value needs encoding; a complete URL usually needs careful assembly, not blind transformation. If you see broken ampersands, doubled percent signs, or a route that looks like it was put through a paper shredder, you probably encoded the wrong layer.
For logs, the choice is often not “which transformation?” but “should this value appear at all?” If the answer is no, remove or mask it. If the answer is “we need to correlate events without storing the raw value,” use a stable hash or HMAC and keep the secret that makes the fingerprint useful under proper control.
PHP Examples: When To Use Built-Ins vs Libraries
I prefer built-ins for the obvious cases because they are easier to audit and harder to misread. I prefer libraries or dedicated extensions when the problem is cryptographic and the API needs to guide you toward safe defaults. That keeps the code honest. Honest code is underrated.
Encoding examples
Encoding is usually the simplest part of the puzzle, but it is also the easiest place to accidentally use the wrong helper for the destination.
$query = http_build_query([
'search' => 'Fish & Chips / A+B',
'page' => 2,
]);
$url = '/blog/?' . $query;
$title = htmlspecialchars($userTitle, ENT_QUOTES, 'UTF-8');
$slugPart = rawurlencode($slugPartRaw);
The useful habit here is not memorizing helpers. It is matching the helper to the destination. Query string? Use query-string tooling. HTML text or attributes? Escape for HTML output. Path segment? Encode the segment, not the whole URL, and not the application’s sense of self-worth.
For Base64, PHP’s base64_encode() manual is the right reference. The function is useful when you need a text representation of binary data or you need to pass binary-ish content through a text-only channel. It is not useful when someone wants to call it security and go home early.
Hashing examples
PHP’s hash() family is useful for fingerprints, checksums, and comparison workflows. It is not the password-storage answer by itself. That is a separate problem and PHP gives you a separate API for it.
$fingerprint = hash('sha256', $documentContents);
$tokenCheck = hash_hmac('sha256', $payload, $serverSecret);
$passwordHash = password_hash($password, PASSWORD_ARGON2ID);
$isValid = password_verify($password, $passwordHash);
The first line is a generic fingerprint. The second is a keyed hash that is useful when you want a stable comparison tied to a server secret. The third and fourth lines are the password-safe workflow. I treat them differently because the threat model is different. If everything gets the same hash, the code tends to get the same mistake.
There is also an important boundary around hashing versus HMAC. A plain hash tells you whether two values are the same. An HMAC tells you whether a value matches a secret-backed expectation. That extra secret makes the fingerprint much more useful when the data might travel or be echoed back through a client-controlled channel.
Encryption examples
When data must be recoverable later, you want encryption with a real key story, not a decorative algorithm name. For PHP, the two common routes are OpenSSL and libsodium.
The official openssl_encrypt() manual is worth reading carefully because the details matter: key length, IV length, cipher mode, and how the authentication tag is handled if you use an authenticated mode like GCM. The API is fine when you respect the parameters. That is a very programmer sentence, and it is accurate.
$key = random_bytes(32);
$iv = random_bytes(openssl_cipher_iv_length('aes-256-gcm'));
$tag = '';
$ciphertext = openssl_encrypt(
$plaintext,
'aes-256-gcm',
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
If your runtime supports libsodium, the sodium_crypto_secretbox() family is often the cleaner mental model for app developers because the API nudges you toward authenticated encryption. It still needs good key handling, but it does less accidental damage than a rushed DIY wrapper.
$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key);
$plaintext2 = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
Use libraries or extensions when they reduce the chance of inventing your own mode handling, tag handling, or key storage habits. The goal is not to look more advanced. The goal is to make the correct thing easier to maintain than the incorrect thing.
If you are prototyping a tiny internal admin tool to test these branches, a neutral web app generator can save time on scaffolding. It does not choose the crypto for you, which is good, because that would be a very expensive misunderstanding.
Common Mistakes To Avoid, And What To Do Instead
- Do not use MD5 for password storage. Use
password_hash()andpassword_verify(). Password storage is not a place to be nostalgic. - Do not assume Base64 is security. If the value must stay confidential, use encryption. Base64 is just representation.
- Do not encrypt something that only needs comparison. Hashing is simpler and usually the right shape for that problem.
- Do not encode the whole URL if only one component is dynamic. Assemble the URL cleanly and encode the parts that need it.
- Do not log secrets because the logs are internal. Internal logs still get copied, shipped, retained, and searched. That is how systems become archivists.
- Do not encrypt without thinking about integrity. Confidentiality alone is not enough if tampering matters. Prefer authenticated encryption patterns.
- Do not store already-escaped display text as your canonical source. Keep raw data canonical and transform at output time.
- Do not call a value “safe” just because it looks encoded. Safe for transport is not safe for disclosure.
The biggest real-world failure mode is usually not the algorithm. It is the mismatch between the algorithm and the job. A clean design starts with the job. Once that is right, the function name is mostly paperwork.
Checklist: How To Verify Your Approach In A Small Test
When I am unsure, I build a tiny test that makes the data travel through the exact destinations I care about. That is faster than arguing with a template from memory.
- Pick one sample value with awkward characters.
Use something likeFish & Chips / "special" + testso the test exposes spaces, quotes, slashes, and plus signs. - Record the raw input once.
Log or dump the value before any transformation so you know what started the journey. - Apply one transformation at a time.
First test encoding, then hashing, then encryption. Do not stack them randomly just because all three are available. - Inspect the final destination.
Check the rendered HTML, the generated URL, the stored hash, or the encrypted payload separately. - Confirm reversibility only where it should exist.
Encrypted data should decrypt with the right key. Encoded data should decode. Hashes should not have a reverse step. - Check logs and reports for accidental disclosure.
If the value still appears in plain form, your test is not done. - Repeat once with a new edge case.
Try a Unicode character, a very long string, or a value containing a percent sign to catch the next layer of trouble.
| Test case | Expected behavior | What would count as a problem |
|---|---|---|
| Password input | Stored as a password hash, verified with password_verify(). |
Plain text, MD5, or reversible encoding. |
| Query parameter | Appears URL-safe and survives browser navigation intact. | Double-encoding, broken separators, or visible HTML entities in a URL. |
| Secret note | Can be recovered only with the expected key and format. | Reversible without the key or logged in clear text. |
| Fingerprint field | Matches when the source value matches, without exposing the source value. | Different fingerprints for the same input or raw secrets in reports. |
If you want a place to document the result of that test, the Support page is the right fallback for a simple implementation review. If you want to keep the operational view of the data flow in sight, the Reports & Tracking section gives you the right mental model: what moved, where it moved, and whether you accidentally shipped the wrong shape.
FAQ
Can I decode a hash?
No. A hash is one-way. You can compare it, verify it, or try candidate inputs against it. You cannot normally reverse it into the original value. That is the whole point. If a system claims a reversible hash, it is usually talking about encryption or a misuse of terms.
Should I encrypt passwords?
No. Passwords should be hashed with a password-specific function like password_hash(). Encryption is for values you must recover later. Passwords are not supposed to be recoverable. If they are, the design is already leaning in the wrong direction.
Is Base64 security?
No. Base64 is encoding. It changes how data is represented so it can travel through text-based systems. Anyone who receives it can decode it. That is useful for transport. It is not useful as a security boundary.
When is MD5 still acceptable?
Only in narrow non-security comparison workflows where legacy compatibility or a checksum-like fingerprint is the actual goal and the threat model is modest. It is not the answer for passwords, and it is not a substitute for modern password hashing or encrypted storage. If you are not sure, assume you should not use it.
Final Takeaway
The cleanest way to choose is to ask one sentence before you write the code: Do I need to recover this later, compare it later, or just move it safely through a text boundary? That sentence points to encryption, hashing, or encoding without the usual ceremony.
When the answer is still fuzzy, review the data path in Sources/Functions…, inspect the trail in Reports & Tracking, or send the implementation through Support for a sanity check before it becomes a cleanup ticket. Small review now, fewer apologies later.