Laravel's String Methods: Clean Inverse Matching

String validation logic in Laravel often required negating startsWith()
and endsWith()
calls, leading to less readable conditions. The new doesntStartWith()
and doesntEndWith()
methods flip this pattern for better code clarity.
From Awkward Negations to Expressive Methods
Previously, checking if a string didn't start or end with certain patterns required negating the existing methods, which could make your conditions harder to read:
use Illuminate\Support\Str;
// The old negation approach
if (!Str::startsWith($filename, ['temp_', 'cache_', 'log_'])) {
// Process permanent file
}
if (!Str::endsWith($email, ['@spam.com', '@blocked.com'])) {
// Valid email domain
}
The new inverse methods provide cleaner, more expressive alternatives:
// Clean and readable
if (Str::doesntStartWith($filename, ['temp_', 'cache_', 'log_'])) {
// Process permanent file
}
if (Str::doesntEndWith($email, ['@spam.com', '@blocked.com'])) {
// Valid email domain
}
Both methods work with single strings and arrays of strings, maintaining the same flexibility as their positive counterparts while eliminating mental overhead from double negatives.
Real-World Example
Consider a file processing system that needs to handle different types of uploads with various validation rules. You need to filter out temporary files, validate file extensions, and ensure proper naming conventions:
<?php
namespace App\Services;
use Illuminate\Support\Str;
use Illuminate\Http\UploadedFile;
class FileProcessingService
{
private array $temporaryPrefixes = ['temp_', 'tmp_', 'cache_', 'draft_'];
private array $restrictedExtensions = ['.exe', '.bat', '.cmd', '.scr'];
private array $systemPrefixes = ['system_', 'admin_', 'config_'];
private array $reservedSuffixes = ['_backup', '_temp', '_old'];
public function validateUploadedFile(UploadedFile $file): array
{
$filename = $file->getClientOriginalName();
$errors = [];
// Check for temporary file patterns - much clearer than negation
if (Str::doesntStartWith($filename, $this->temporaryPrefixes)) {
// This is a permanent file, continue validation
} else {
$errors[] = 'Temporary files cannot be uploaded permanently';
}
// Validate against restricted file extensions
if (Str::doesntEndWith($filename, $this->restrictedExtensions)) {
// Safe file extension, proceed
} else {
$errors[] = 'Executable files are not allowed';
}
// Ensure user files don't use system prefixes
if (Str::doesntStartWith($filename, $this->systemPrefixes)) {
// User-safe filename prefix
} else {
$errors[] = 'System-reserved filenames are not allowed';
}
// Check for reserved suffixes before extension
$nameWithoutExt = pathinfo($filename, PATHINFO_FILENAME);
if (Str::doesntEndWith($nameWithoutExt, $this->reservedSuffixes)) {
// Clean filename
} else {
$errors[] = 'Files cannot end with reserved suffixes';
}
return [
'valid' => empty($errors),
'errors' => $errors,
'filename' => $filename
];
}
public function categorizeFiles(array $filenames): array
{
$categories = [
'user_uploads' => [],
'temporary_files' => [],
'system_files' => [],
'safe_executables' => []
];
foreach ($filenames as $filename) {
// Clear categorization logic using positive conditions
if (Str::doesntStartWith($filename, $this->temporaryPrefixes)) {
if (Str::doesntStartWith($filename, $this->systemPrefixes)) {
$categories['user_uploads'][] = $filename;
} else {
$categories['system_files'][] = $filename;
}
} else {
$categories['temporary_files'][] = $filename;
}
// Safe executables are those that don't end with restricted extensions
if (Str::doesntEndWith($filename, $this->restrictedExtensions)) {
$categories['safe_executables'][] = $filename;
}
}
return $categories;
}
public function sanitizeUserInput(string $content): array
{
$suspiciousPatterns = ['<script', '<?php', 'javascript:', 'data:text/html'];
$unsafeProtocols = ['ftp://', 'file://', 'javascript:'];
$validation = [
'is_safe_content' => Str::doesntStartWith(
Str::lower($content),
array_map('strtolower', $suspiciousPatterns)
),
'has_safe_protocols' => Str::doesntStartWith(
Str::lower($content),
$unsafeProtocols
),
'clean_text' => true
];
// Additional checks for text endings
$dangerousEndings = ['.exe', '.bat', '.js', '.vbs'];
if (preg_match('/\.[a-z]{2,4}$/i', $content, $matches)) {
$validation['safe_file_reference'] = Str::doesntEndWith(
Str::lower($matches[0]),
$dangerousEndings
);
}
return $validation;
}
public function processEmailDomains(array $emails): array
{
$blockedDomains = ['@spam.com', '@blocked.com', '@tempmail.org'];
$internalDomains = ['@company.com', '@internal.local'];
$processed = [
'external_safe' => [],
'internal' => [],
'blocked' => []
];
foreach ($emails as $email) {
if (Str::doesntEndWith(Str::lower($email), $blockedDomains)) {
if (Str::doesntEndWith(Str::lower($email), $internalDomains)) {
$processed['external_safe'][] = $email;
} else {
$processed['internal'][] = $email;
}
} else {
$processed['blocked'][] = $email;
}
}
return $processed;
}
}
// Usage in controllers and middleware
class FileUploadController extends Controller
{
public function __construct(
private FileProcessingService $fileService
) {}
public function store(Request $request)
{
$file = $request->file('upload');
$validation = $this->fileService->validateUploadedFile($file);
if (!$validation['valid']) {
return back()->withErrors([
'upload' => $validation['errors']
]);
}
// Store the file with confidence it passed all validations
$path = $file->storeAs('uploads', $validation['filename']);
return redirect()->route('files.index')
->with('success', 'File uploaded successfully');
}
public function bulkProcess(Request $request)
{
$filenames = $request->input('filenames', []);
$categories = $this->fileService->categorizeFiles($filenames);
return response()->json([
'categorized_files' => $categories,
'user_uploads_count' => count($categories['user_uploads']),
'system_files_count' => count($categories['system_files'])
]);
}
}
The doesntStartWith()
and doesntEndWith()
methods make these validation rules significantly more readable than their negated counterparts. Instead of having to mentally parse !Str::startsWith()
conditions, developers can read the code more naturally: "if this doesn't start with temp prefixes" or "if this doesn't end with blocked domains."
This is particularly valuable in complex validation chains where multiple negative conditions might be combined. The fluent string API also supports these methods, making them available in method chaining scenarios:
$result = str($filename)
->lower()
->doesntStartWith(['temp_', 'cache_'])
->doesntEndWith(['.tmp', '.cache']);
These methods follow the same pattern as their positive counterparts, accepting both single strings and arrays of potential matches, making them drop-in replacements for negated logic throughout your codebase.
Stay Updated with More Laravel Tips
Enjoyed this article? There's plenty more where that came from! Subscribe to our channels to stay updated with the latest Laravel tips, tricks, and best practices: