The go-to resource for learning PHP, Laravel, Symfony, and your dependencies.

Doctrine ORM Upgrade Best Practices


Imagine you’re responsible for a historic stone bridge— one that has carried traffic reliably for years. The underlying technology is sound, the foundation is solid, but the stones are beginning to show their age. Replacement parts are scarce. Newer bridges use modern materials and designs that offer better performance and safety. Your task: to upgrade this bridge while it’s still in use, without disrupting the daily flow of commerce.

This, in essence, is the challenge of upgrading Doctrine ORM in a production PHP application. The ORM forms a critical bridge between your domain logic and the database— a piece of infrastructure that, once deployed, touches nearly every database operation. Upgrading it requires careful planning, thorough testing, and a deep understanding of both the improvements you’re gaining and the breaking changes you must accommodate.

In this guide, we’ll walk through proven strategies for navigating Doctrine ORM upgrades. We’ll examine why these upgrades matter, how to prepare, what breaking changes to expect, and how to validate your migration. By the end, you should have a clear roadmap for upgrading your own applications with confidence.

Why Doctrine Upgrades Matter

Doctrine ORM is a foundational component in many PHP applications— particularly those using Symfony, Laravel, or custom architectures that require sophisticated object-relational mapping. When we delay upgrades, we accumulate technical debt in several ways:

  • Security vulnerabilities may go unpatched in older versions
  • Performance improvements in newer versions remain untapped
  • PHP compatibility issues emerge as PHP itself evolves
  • Community support dwindles for outdated versions
  • New features that could simplify your codebase remain unavailable

Of course, upgrading is not without risk. Breaking changes between major versions—particularly from Doctrine 2.x to 3.x, or from 3.x to 4.x—can require substantial code modifications. The upgrade process itself may reveal design issues that have been lurking in your domain model for years.

Before we get into the specific steps, though, let’s acknowledge a fundamental truth: not every application needs to upgrade immediately. If your application is stable, secure, and not requiring new Doctrine features, you may choose to stay on your current version until end-of-life approaches. That’s a valid decision. But when you do decide to upgrade—and that time will come—you’ll want a systematic approach.

Understanding Doctrine Versioning

Doctrine follows semantic versioning, but with important nuances for ORM and DBAL (Database Abstraction Layer). Let’s clarify the relationship:

  • Doctrine ORM depends on Doctrine DBAL—you typically upgrade both together
  • Major version jumps (2.x → 3.x, 3.x → 4.x) introduce breaking API changes
  • Minor version updates (3.6 → 3.7) generally maintain backward compatibility within the major version
  • DBAL sometimes introduces breaking changes independently of ORM

For example, when upgrading from Doctrine ORM 2.14 to 3.0, you’re also upgrading DBAL from 3.x to 4.x. The deprecations and removals span both layers. We’ll examine the most common breaking changes shortly.

Pre-Upgrade Assessment

Before modifying any code, we need to understand our current state. A disciplined upgrade begins with measurement, not modification.

Inventory Your Current Setup

First, let’s determine exactly what versions we’re running:

# Check your composer.json for doctrine/orm and doctrine/dbal
cat composer.json | grep -E "doctrine/(orm|dbal)"

# Or check installed versions directly
composer show doctrine/orm doctrine/dbal

You might see output like:

doctrine/orm          2.14.1  Object-Relational Mapper for PHP
doctrine/dbal         3.6.0   Database Abstraction Layer

Take note of:

  • Your current PHP version (Doctrine may require newer PHP)
  • Your database platform (MySQL 5.7 vs 8.0, PostgreSQL version, etc.)
  • Any custom DBAL types or hydration modes you’ve registered
  • Third-party bundles that bundle Doctrine (e.g., doctrine/doctrine-bundle for Symfony)

Check Compatibility Matrix

Doctrine maintains upgrade guides that document breaking changes. We should review these before proceeding:

These guides enumerate deprecations and required code changes. But reading them wholesale is overwhelming. A practical approach is to search your codebase for common breaking patterns, which we’ll discuss next.

Static Analysis with Rector

One highly effective approach is to use Rector with Doctrine upgrade rules. Rector can automatically fix many breaking changes— particularly method signature changes, removed methods, and renamed classes.

You might use DBAL upgrades alone if no entities change. Or Rector for automated fixes. These complement manual steps but test outputs.

Let’s see how this works. First, install Rector with Doctrine rules:

composer require rector/rector --dev
composer require rector/doctrine --dev

Then, create a rector.php configuration:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Doctrine\Set\DoctrineSetList;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([
        __DIR__.'/src',
        __DIR__.'/config',
    ]);
    
    // Apply Doctrine upgrade rules (e.g., ORM 2.x → 3.0)
    $rectorConfig->sets([DoctrineSetList::ORM_30]);
    
    // Run in dry-run mode first to see changes
    $rectorConfig->dryRun();
    $rectorConfig->reportDiffs(__DIR__.'/rector-diff.log');
};

Now, run Rector in dry-run mode to see what it would change:

vendor/bin/rector process

Examine rector-diff.log to understand the transformations. When you’re ready, remove dryRun() or set it to false and run again to apply changes.

Important caveat: Rector is incredibly useful but not infallible. It can’t fix every breaking change— particularly those involving changed semantics or removed functionality that require human judgment. Always review Rector’s changes carefully.

Common Breaking Changes and How to Address Them

Let’s examine the most frequently encountered breaking changes when upgrading from Doctrine ORM 2.x to 3.x (and DBAL 3.x to 4.x). If you’re upgrading from an earlier version— say, 2.7 to 2.14—the changes will be less severe but still worth reviewing.

Entity Manager Changes

The EntityManager interface underwent significant cleanup in ORM 3.0. Several methods were removed or replaced:

Removed/Changed MethodReplacement
getUnitOfWork()Use getEntityChangeSet() or getOriginalEntityData() via the UnitOfWork directly if needed
getClassMetadata($className)Still exists but returns ClassMetadata in namespace Doctrine\ORM\Mapping
getConnection()Still exists, but pay attention to DBAL 4 changes
beginTransaction() / commit() / rollback()Move to $entityManager->getConnection()->beginTransaction()

Let’s look at a concrete example. Suppose we had this code in our 2.x application:

$em = $this->getEntityManager();
$em->getUnitOfWork()->computeChangeSets();

In Doctrine 3.x, computeChangeSets is called automatically. If we were using getUnitOfWork() to inspect changes, we should instead use the new getEntityChangeSet() method:

$em = $this->getEntityManager();
$changeSet = $em->getUnitOfWork()->getEntityChangeSet($entity);

Wait— that’s still using getUnitOfWork(). The difference is that what you can do with the UnitOfWork has been reduced. Let’s refine: if we need change tracking, we should now rely on getEntityChangeSet(), getOriginalEntityData(), and related methods, which are available on the EntityManagerInterface:

$changeSet = $em->getEntityChangeSet($entity);
$originalData = $em->getOriginalEntityData($entity);

Much cleaner. But note: if your code previously relied on internal UnitOfWork methods that no longer exist, you’ll need to refactor the logic.

Connection and SQL Changes

DBAL 4.0 removed many legacy methods. The most common issue is with executeQuery() and executeStatement() signatures:

DBAL 2.x / 3.x:

$connection->executeQuery('SELECT * FROM users WHERE id = ?', [$id]);
$connection->executeUpdate('UPDATE users SET name = ? WHERE id = ?', [$name, $id]);

DBAL 4.0+ (which ships with ORM 3.0+):

$connection->executeQuery('SELECT * FROM users WHERE id = ?', [$id])->fetchAllAssociative();
$connection->executeStatement('UPDATE users SET name = ? WHERE id = ?', [$name, $id]);

Notice: executeUpdate became executeStatement. The return type changed from int (affected rows) to a Result object. If you need the count, use:

$result = $connection->executeStatement('UPDATE users SET name = ? WHERE id = ?', [$name, $id]);
$affectedRows = $result->rowCount();

Additionally, many SQL builders and column types changed. For example, the SimpleXMLElement DBAL type was removed. If you used it, you’ll need to replace it with a custom type or another solution.

Platform-Specific Changes

If you’re using Oracle, MySQL 5.7, or SQL Server older versions, pay special attention to platform-specific behavior. Doctrine DBAL 4.0 dropped support for several older database platforms:

  • MySQL 5.5 and earlier
  • SQLite < 3.8.3
  • Some legacy Oracle versions

Check the DBAL documentation for your specific platform. You might need to upgrade your database server in parallel with Doctrine.

Third-Party Bundles

If you’re using Symfony with doctrine/doctrine-bundle, ensure you’re on a version compatible with your target ORM version:

  • doctrine/doctrine-bundle 2.4+ supports Doctrine ORM 3.0
  • Older bundles will throw class-not-found errors

Similarly, if you use stof/doctrine-extensions-bundle for Timestampable, Sluggable, etc., you need stof/doctrine-extensions-bundle 2.0+ for ORM 3.0 compatibility.

We’ll discuss dependency management in more detail below.

The Upgrade Process: A Step-by-Step Walkthrough

Let’s build a practical upgrade workflow. We’ll assume we’re upgrading from Doctrine ORM 2.14 to 3.0, but the principles apply to other version jumps as well.

Step 1: Create a Baseline

Before touching anything, ensure we have a clean, working state:

git checkout main
git pull origin main
git status  # should show no uncommitted changes

If you’re not using Git, at least copy your current working directory to a backup location. We’re going to make potentially breaking changes; we need the ability to revert.

Step 2: Update composer.json

We need to specify the new Doctrine version constraints. The simplest approach is to let Composer resolve the latest compatible versions:

{
    "require": {
        "doctrine/orm": "^3.0",
        "doctrine/dbal": "^4.0"
    }
}

But note: this will upgrade to the latest 3.x and 4.x versions, which may include minor breaking changes. If we want to be more conservative, we can pin to the first 3.0 release:

{
    "require": {
        "doctrine/orm": "~3.0.0",
        "doctrine/dbal": "~4.0.0"
    }
}

Let’s also check for transitive dependencies—do any of our other packages require a specific Doctrine version? Composer will tell us if there are conflicts.

Step 3: Dry-Run the Dependency Resolution

Before actually installing, let’s see what Composer plans to do:

composer update doctrine/orm doctrine/dbal --dry-run

This shows which packages would be installed, removed, or updated. Look for:

  • Packages that would be removed (maybe a bundle that’s incompatible)
  • Version jumps that seem excessive
  • Any conflicts that prevent resolution

If the dry-run looks reasonable, we proceed— but we still won’t modify our code yet. First, let’s get the new dependencies installed in a separate branch.

git checkout -b doctrine-upgrade-30
composer update doctrine/orm doctrine/dbal

Now we have the new libraries installed. Composer will have updated composer.lock. Commit this:

git add composer.json composer.lock
git commit -m "Upgrade Doctrine ORM to 3.0 and DBAL to 4.0"

Step 4: Run Test Suite

Now— before changing any application code— run your test suite:

# If using PHPUnit
./vendor/bin/phpunit

# If using Pest
./vendor/bin/pest

# Or your project's test command

At this point, you’ll likely see failures. The deprecation notices alone may cause test failures if you have E_ALL error reporting. Don’t panic— this is expected. We’re just getting a baseline of what breaks.

What we’re looking for:

  • Fatal errors (class not found, method not found)
  • Deprecation warnings (Doctrine emits many in 2.x->3.x transition)
  • Behavioral regressions (tests that pass before but fail now)

Document the failures. Categorize them as:

  1. Type 1: Simple method/class renames or removals (Rector can likely fix)
  2. Type 2: Changed signatures or return types (manual fix needed)
  3. Type 3: Behavioral changes (requires understanding and possible redesign)
  4. Type 4: Third-party incompatibilities (need to upgrade other packages)

Step 5: Apply Automated Fixes

Now let’s run Rector with appropriate Doctrine sets. We’ve already configured rector.php above. Run it:

vendor/bin/rector process

Rector will apply its transformations and report changed files. Review the changes carefully— particularly if you have custom repositories or complex query logic.

After Rector runs, commit:

git add .
git commit -m "Apply Rector automated fixes for Doctrine ORM 3.0"

Step 6: Manual Code Changes

Rector doesn’t catch everything. Let’s tackle the manual fixes systematically.

First, address deprecation notices. Run your test suite again:

./vendor/bin/phpunit 2>&1 | grep -i "deprecated"

Or, if you have a way to collect all deprecations into a log file:

./vendor/bin/phpunit --log-junit deprecations.xml 2>&1 | tee phpunit.log

Then examine the log for “deprecated”. Each deprecation is a hint of code that needs updating. Common patterns:

  • Doctrine\Common\Cache\CacheProvider was removed— use PSR-6 or PSR-16 cache implementations instead
  • Doctrine\ORM\Tools\SchemaTool methods that take $saveMode parameter changed
  • EntityRepository::createQueryBuilder() signature removed the $alias parameter in some contexts

Second, check your custom DBAL types. If you registered custom types via Connection::registerDoctrineTypeMapping() or similar, the type registration API changed:

// Doctrine DBAL 3.x
$conn->getConfiguration()->setCustomDoctrineTypes([
    'my_custom_type' => MyCustomType::class,
]);

// Doctrine DBAL 4.0+ - slightly different
$conn->getConfiguration()->registerCustomDoctrineType('my_custom_type', MyCustomType::class);

But the exact method depends on how you’re implementing Type subclasses. Consult the DBAL upgrade guide.

Third, review your DQL queries. Some DQL functions were removed or renamed. For example, IDENTITY() function behavior changed slightly. If you use native SQL queries with ResultSetMapping, verify those mappings still work.

Step 7: Update Third-Party Packages

Now let’s check our third-party dependencies. Run:

composer outdated

Look for packages that are not compatible with Doctrine ORM 3.0. Common ones:

  • doctrine/doctrine-bundle (if using Symfony)
  • stof/doctrine-extensions-bundle
  • gedmo/doctrine-extensions
  • api-platform/core (if using API Platform)

Upgrade them one by one, checking compatibility matrices. For Symfony users:

composer require doctrine/doctrine-bundle:^2.4

The “strictly speaking” version is: 2.4 introduced ORM 3.0 support, but later patch releases fix issues. I’d recommend ^2.9 at the time of writing for best compatibility.

After upgrading each package, run your test suite again. Some bundles may require configuration changes—consult their upgrade guides.

Step 8: Integration Testing with Real Data

Unit tests are necessary but not sufficient. We must test with realistic data volumes and actual database schema.

First, dump a copy of your production database (or a sanitized backup) to a local test database:

mysqldump -u root -p production_db > prod_backup.sql
mysql -u root -p test_db < prod_backup.sql

Or use your database’s preferred dump/restore mechanism.

Now, run integration tests that touch the database:

./vendor/bin/phpunit --group integration

If you don’t have integration tests, create some that:

  • Load your actual entity mappings
  • Perform common CRUD operations
  • Test queries that involve joins, subqueries, complex DQL
  • Verify cached queries still work (if you use query caching)

Watch for:

  • Schema mismatches (maybe your schema tool generated different SQL)
  • Slow queries (new ORM might generate different query plans)
  • Missing indexes (sometimes schema diff tools miss things)

Step 9: Performance Validation

Doctrine ORM 3.0 and DBAL 4.0 include performance improvements, but they can also expose N+1 query problems that were previously hidden.

Run a profiling tool—Xdebug profiler, Blackfire, or XHProf—on representative request cycles. Compare query counts and execution times between old and new versions.

If you see significant regressions:

  • Check for lazy loading issues
  • Verify your fetch joins are still working as expected
  • Examine if you need to adjust hydration modes

Tip: For Symfony users, symfony prof:start can help identify queries during a request.

Step 10: Gradual Rollout Strategy

If this is a production system with high availability requirements, don’t upgrade everything at once. Consider:

  1. Canary deployment: Deploy to a small subset of servers first
  2. Feature flags: Wrap new Doctrine-dependent code behind flags so you can disable quickly
  3. Database backups: Ensure you have backups and a rollback plan
  4. Monitoring: Set up alerts for database error rates, slow queries, and application exceptions

For Symfony projects, symfony check:security post-upgrade helps identify known security issues—run it as part of your checklist.

Handling Specific Breaking Scenarios

Let’s dive deeper into some specific issues that commonly trip up upgrades.

Case: Schema Tool Deprecations

The Doctrine SchemaTool API changed significantly. If you use schema migrations (via doctrine/migrations), you may need to update how you generate migration diff:

# Before (Doctrine Migrations 2.x/3.x with ORM 2.x)
php bin/console doctrine:migrations:diff

# After (with Migrations 3.x and ORM 3.x)
php bin/console doctrine:migrations:diff

The command is the same, but the underlying API changed. If you see “Call to undefined method” errors, you likely have a custom migration generator or event subscriber that hooks into the schema tool. Update those to use the new Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs instead of the old EventArgs.

Case: Removed Result Cache

Doctrine ORM 3.0 removed the built-in result cache implementation. If you were using:

$query->useResultCache(true, 3600, 'my_cache_key');

This no longer works as expected. Instead, you need to implement PSR-6 or PSR-16 caching and integrate it via query hints:

use Symfony\Component\Cache\Adapter\RedisAdapter;
use Doctrine\ORM\Query;

$cache = RedisAdapter::createConnection('redis://localhost');
$query->setHint(Query::HINT_CACHE_OUTPUT, $cache);

The exact mechanism depends on your caching library. Doctrine now defers to the cache pool backend rather than providing its own.

Case: QueryBuilder Chain Methods

Some QueryBuilder chain methods were removed or changed. For example, expr()->in() used to accept array parameters directly in some versions, but now you need:

$qb->where($qb->expr()->in('u.id', ':ids'))
   ->setParameter('ids', $idArray);

The behavior is more consistent now, but if you were relying on the old array-expansion behavior, you’ll need to pass a parameter.

Alternative Approaches

We’ve focused on the manual, systematic upgrade path. But there are alternatives worth considering depending on your situation.

DBAL-Only Upgrades

If your entities don’t need changes—perhaps you’re on Doctrine ORM 2.14 and just want DBAL 4.0 for security patches—you might upgrade DBAL alone. This is relatively safe, as DBAL 4.0 maintains backward compatibility with ORM 2.x.

composer require doctrine/dbal:^4.0

Run tests. Generally this works because DBAL maintains BC with recent ORM versions. But check the DBAL upgrade guide for any removed features that your ORM might depend on.

Trade-off: Simpler migration, but you’ll eventually need to upgrade ORM anyway. If you’re on ORM 2.14 (the last 2.x release), you may want to upgrade both together rather than prolonging the transition.

Rector-Driven Full Upgrade

We already discussed Rector for automated fixes. But you can lean on it more heavily: run it on your entire codebase, commit the changes, and then manually review the diff. For large codebases, this can save hundreds of hours.

The risk: Rector might make incorrect assumptions in edge cases. Always review its changes thoroughly and test extensively.

Replatforming: Consider Alternatives

Of course, not every project must stay with Doctrine. If the upgrade effort is enormous—perhaps your codebase uses deeply deprecated patterns that would require a full rewrite anyway—consider:

  • Switching to Eloquent (Laravel’s ORM)
  • Using plain PDO with a lightweight query builder
  • Adopting anemic domain models with service layer (Doctrine not needed)

But be honest: these replatforming efforts often take longer than a careful Doctrine upgrade. Unless you have a compelling reason to switch, upgrading Doctrine is usually the pragmatic choice.

Troubleshooting and Edge Cases

Let’s address some common issues that arise mid-upgrade.

Composer Conflicts

If Composer refuses to resolve dependencies, we have a few options:

  1. Identify the blocker: Composer will tell you which package requires an incompatible Doctrine version
  2. Search for replacement packages: Maybe there’s a newer version of that package that supports ORM 3.0
  3. Temporarily remove non-essential packages: Comment them out in composer.json, upgrade Doctrine, then see if they work anyway or can be replaced
  4. Fork and patch: As a last resort, fork an incompatible package and adjust its composer.json to accept your Doctrine version, then adjust its code for compatibility

Persistent “Class Not Found” Errors After Composer

If you see errors like Class 'Doctrine\ORM\Tools\Pagination\Paginator' not found, double-check:

  • Composer autoloader is updated: composer dump-autoload
  • The package is actually installed: composer show doctrine/orm
  • You don’t have a conflicting global Composer install

Sometimes IDE caches cause false positives. Clear your IDE’s cache if you see classes that compose insists are there but your IDE says are missing.

Performance Regressions After Upgrade

Doctrine ORM 3.0 includes changes to hydration strategies. If you see queries taking longer:

  • Verify that indexes exist on foreign key columns
  • Check your fetch mode settings (eager vs lazy loading)
  • Look for queries that now return more columns than needed
  • Consider using partial objects or scalar hydration for read-only operations

Run EXPLAIN on slow queries directly in the database to see if the query plan changed.

Deprecation Noise Overload

Doctrine ORM 2.x emits thousands of deprecation notices in the strictest mode. To make the noise manageable:

// In your test bootstrap or config
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);

But don’t silence them permanently—you need to address deprecations before upgrading. Use a deprecation detector tool:

composer require symfony/phpunit-bridge --dev
vendor/bin/simple-phpunit --coverage-text  # can detect deprecations

Or use PHPStan with Doctrine extensions:

composer require phpstan/phpstan-doctrine --dev
vendor/bin/phpstan analyse src

PHPStan will catch many Doctrine deprecations statically.

Post-Upgrade Checklist

Once tests pass and integration validation is complete, we need to finalize the upgrade.

Documentation Updates

Update any project documentation that references Doctrine APIs:

  • README files with code examples
  • Internal wiki pages about database operations
  • Onboarding materials for new developers

Particularly check for references to removed methods like getUnitOfWork()->computeChangeSets().

Dependency Bump Beyond Doctrine

Now that we’ve upgraded ORM and DBAL, let’s check if we can—or should—upgrade other related packages:

  • symfony/orm-pack (if using Symfony Flex)
  • doctrine/doctrine-migrations-bundle
  • Any custom code that extends Doctrine classes (repositories, event listeners, types)

Each may have its own compatibility constraints.

Production Monitoring Plan

Before deploying to production, set up monitoring:

  • Database error rates (from application logs)
  • Query performance metrics (slow query log, application performance monitoring)
  • Memory usage (Doctrine can be memory-intensive)
  • Cache hit rates (if using query cache)

You want to know immediately if the upgrade introduces issues that tests didn’t catch.

Long-Term Considerations and Trade-offs

We’ve focused on the mechanics of a single upgrade. But let’s step back and consider the broader implications.

The Upgrade Cadence

How often should you upgrade Doctrine? The answer depends on your risk tolerance and resources:

  • Conservative: Upgrade once every 2-3 years, skipping some major versions. Pro: less frequent disruption. Con: larger diffs, more breaking changes to handle at once.
  • Moderate: Upgrade within a year of each major release. Pro: smaller incremental changes. Con: more frequent upgrade cycles.
  • Aggressive: Upgrade as soon as minor version deprecations appear, using deprecation warnings to keep code current. Pro: never face a huge upgrade. Con: continuous minor effort.

I’d recommend the moderate approach for most production systems. Track Doctrine’s release cycle and start planning upgrades when the current version you’re on approaches end-of-life (usually 2-3 years after release).

The Cost of Staying Behind

Each year you stay on an old Doctrine version, you accumulate hidden costs:

  • Security patches may require backporting or custom fixes
  • New PHP features (like readonly properties, enums) may not integrate cleanly
  • Third-party packages stop supporting your version
  • You miss performance enhancements that could reduce infrastructure costs

Quantify these costs when deciding upgrade priority.

The Value of Deprecation Warnings

Strictly speaking, you should never ignore deprecation warnings. They are your early warning system—designed to give you months or years to adapt before a method actually vanishes.

Configure your development and CI environments to fail on deprecations:

# phpunit.xml
<php>
    <ini name="error_reporting" value="E_ALL"/>
    <ini name="display_errors" value="1"/>
</php>

Then treat deprecation-notice test failures as bugs. Fix them as they appear. This turns upgrades from crisis events into routine maintenance.

Conclusion

Upgrading Doctrine ORM is rarely trivial—but it’s rarely catastrophic either, assuming you follow a disciplined approach. The key steps are:

  1. Assess your current state and breaking changes
  2. Automate what you can with Rector
  3. Fix deprecations early and often
  4. Test with real data and integration scenarios
  5. Plan your rollout with rollback capability

You also can use DBAL upgrades alone if no entities change. Or Rector for automated fixes. These complement manual steps but test outputs.

For Symfony, run symfony check:security post-upgrade to catch any remaining issues.

Remember: the bridge keeper’s duty is not just to maintain the old bridge indefinitely, but to know when—and how—to build a new one. With these practices, you’ll be ready when that time comes.

Sponsored by Durable Programming

Need help with your PHP application? Durable Programming specializes in maintaining, upgrading, and securing PHP applications.

Hire Durable Programming