# WooCommerce to Laravel Migration: The Parts Nobody Talks About

**Author:** Mozex | **Published:** 2026-03-29 | **Tags:** Laravel, PHP, Architecture, WooCommerce | **URL:** https://mozex.dev/blog/6-woocommerce-to-laravel-migration-the-parts-nobody-talks-about

---


If you've run a WooCommerce store long enough, you've felt the slowdown. Pages take longer to load. The admin panel becomes sluggish. Checkout starts timing out during traffic spikes. You throw caching at it, upgrade your hosting, install optimization plugins. It helps for a while. Then it doesn't.

I've been building with PHP and Laravel for over ten years, and I've migrated production WooCommerce stores to custom Laravel applications. Not as a theoretical exercise, but because the stores hit a wall that no amount of WordPress optimization could fix.

Here's what that process actually looks like, including the parts the generic migration guides conveniently skip.

<!--more-->

## The wp_postmeta Problem

Every conversation about WooCommerce performance eventually leads back to one table: `wp_postmeta`.

WordPress uses an Entity-Attribute-Value (EAV) pattern for storing metadata. In practice, this means a single product doesn't live in one row of a products table. It lives in `wp_posts` (one row) plus dozens of rows in `wp_postmeta`, each storing a single key-value pair.

A typical WooCommerce product generates around 29 meta rows. A single order? 40 to 50 rows. Run the math on a store with 100,000 orders and you're looking at 4-5 million rows in `wp_postmeta` alone.

Here's the part that hurts: every value in that table is stored as a string. Prices, dates, foreign keys, serialized PHP arrays. MySQL can't enforce types, can't optimize range queries, and can't build efficient indexes on this structure. When your checkout page needs to look up a customer's shipping address, it's joining through this table. When your admin dashboard lists recent orders, it's joining through this table. When your product search runs, same table.

WooCommerce introduced High-Performance Order Storage (HPOS) to address the order side of this problem, moving orders into dedicated tables. It's a real improvement. But products, customers, coupons, and most custom data still live in the EAV soup.

## What the Migration Actually Involves

Generic guides present WooCommerce-to-Laravel migration as a clean sequence: export your data, import into new tables, build new templates. Done.

The reality is messier.

### Data Transformation, Not Data Transfer

You can't dump WooCommerce's database and load it into Laravel. The schemas have nothing in common. What you're doing is building a translation layer that reads the WordPress/WooCommerce structure and writes normalized Eloquent models.

For products, that means:

```php
// WooCommerce stores a product price like this:
// wp_postmeta: post_id=42, meta_key='_regular_price', meta_value='29.99'
// wp_postmeta: post_id=42, meta_key='_sale_price', meta_value='19.99'
// wp_postmeta: post_id=42, meta_key='_sku', meta_value='WIDGET-001'
// wp_postmeta: post_id=42, meta_key='_stock', meta_value='150'
// ... 25 more rows

// Laravel stores it like this:
$product = Product::create([
    'sku' => 'WIDGET-001',
    'regular_price' => 29.99,  // decimal column
    'sale_price' => 19.99,     // decimal column
    'stock' => 150,            // integer column
]);
```

That looks simple. It isn't. The WooCommerce side has years of accumulated edge cases. Products with missing SKUs. Prices stored as empty strings instead of null. Serialized arrays in meta values that need to be unpacked and mapped to separate relationship tables. Variable products with attributes spread across `wp_postmeta`, `wp_terms`, and `wp_term_relationships`.

I wrote Artisan commands to handle this, and the transformation logic for products alone was several hundred lines. Not because the mapping is conceptually hard, but because production data is never as clean as the schema suggests.

### Password Hashes Don't Match

WordPress 6.8 (April 2025) switched its password hashing from the old phpass algorithm to bcrypt. Good news, right? Laravel uses bcrypt too. Except WordPress doesn't use standard bcrypt. It pre-hashes the password with SHA-384 using an HMAC key, then base64-encodes the result before feeding it to bcrypt. The stored hash gets a `$wp$` prefix to distinguish it from vanilla bcrypt: `$wp$2y$10$...`.

Laravel's `Hash::check()` doesn't know about any of that. It'll fail on every WordPress 6.8+ password hash.

And if you're migrating an older store that hasn't been updated to WP 6.8, you might still have legacy phpass hashes (the ones starting with `$P$`). Those are a completely different algorithm.

Two options:

1. Force a password reset for every customer during migration. Simple, but annoying for users.
2. Write a custom hash checker that handles both WordPress formats, then rehashes to standard Laravel bcrypt on first login.

```php
use Illuminate\Contracts\Auth\Authenticatable;

// In a custom UserProvider
public function validateCredentials(Authenticatable $user, array $credentials): bool
{
    $password = $credentials['password'];
    $hash = $user->getAuthPassword();

    // Standard Laravel bcrypt (already migrated users)
    if (Hash::check($password, $hash)) {
        return true;
    }

    // WordPress 6.8+ bcrypt ($wp$2y$ prefix, SHA-384 pre-hashed)
    if (str_starts_with($hash, '$wp$')) {
        $prehashed = base64_encode(hash_hmac('sha384', trim($password), 'wp-sha384', true));
        $strippedHash = substr($hash, 3); // '$wp$2y$10$...' → '$2y$10$...'

        if (password_verify($prehashed, $strippedHash)) {
            $user->forceFill(['password' => Hash::make($password)])->save();
            return true;
        }
    }

    // Legacy WordPress phpass ($P$ prefix, WP < 6.8)
    if (str_starts_with($hash, '$P$')) {
        $wpHasher = new \MikeMcLin\WpPassword\WpPassword();
        if ($wpHasher->check($password, $hash)) {
            $user->forceFill(['password' => Hash::make($password)])->save();
            return true;
        }
    }

    return false;
}
```

For the legacy phpass check, I'd use [`mikemclin/laravel-wp-password`](https://github.com/mikemclin/laravel-wp-password). Don't use `hautelook/phpass` for this. That package was abandoned in 2021 and then [repo-jacked in a supply chain attack](https://www.concretecms.org/about/project-news/security/supply-chain-hack-phpass-repo-jacking) that injected credential-stealing code. It sat compromised on Packagist for five days before anyone noticed.

Option 2 is more work upfront but invisible to customers. Nobody notices their password got rehashed behind the scenes. I went with this approach every time.

### Payment Gateways Don't Migrate

This one catches people off guard. If your WooCommerce store uses Stripe, you have customer tokens and payment methods stored through WooCommerce's Stripe plugin. Those tokens are vault references tied to the WordPress integration.

You can't just copy the Stripe customer ID to your Laravel database and call it done. You need to:

1. Map WooCommerce customer IDs to your new user records
2. Use Stripe's API to retrieve the customer's payment methods
3. Set up Laravel Cashier (or your own Stripe integration) with those existing Stripe customer references
4. Test actual charges against test mode to confirm the linkage works

For stores using PayPal, the situation is similar but more painful. PayPal's vault tokens and billing agreements have their own migration headaches.

The payment gateway migration usually takes longer than the product data migration. Plan for that.

## The Plugin Gap

WooCommerce has over 800 plugins covering everything from shipping calculators to abandoned cart recovery to dynamic pricing. When you move to Laravel, every plugin becomes one of three things:

1. **A Composer package** that does the same job. These exist for payments (Cashier), email (various), PDF generation, etc. Maybe 20% of what you need.
2. **Custom code you write yourself.** Shipping calculations, tax rules, discount logic, inventory management. This is the bulk of the work.
3. **A third-party API** you integrate directly. For shipping rates, you call the carrier APIs. For email marketing, you hit Mailchimp or ConvertKit's API. Cleaner than plugins, but you build and maintain the integration.

The gap is real. A WooCommerce store that relies heavily on plugins for core business logic will take significantly longer to migrate than one that mostly uses WooCommerce out of the box.

On the other hand, every plugin you replace with custom code is a plugin you no longer worry about breaking on the next WordPress update. That stability is worth something.

## SEO Preservation

If the store has any organic traffic, URL structure matters. WooCommerce's default product URL pattern is `/product/slug-name/`. Category pages live at `/product-category/category-name/`. You probably have custom redirects from old slugs, pagination URLs indexed in Google, and canonical tags set by Yoast or RankMath.

During migration:

- Map every old URL to its new equivalent
- Set up 301 redirects for every URL that changes structure
- Preserve product slugs wherever possible
- Submit an updated sitemap to Search Console immediately after launch
- Monitor 404 errors aggressively for the first few weeks

A bulk redirect file with 5,000+ entries is normal for a large store. Don't treat this as an afterthought. I've seen migrations where traffic dropped 40% because someone forgot to redirect the old category URLs.

## What Gets Better Immediately

After all the migration pain, the difference in performance alone makes it worth it.

**Query performance.** A product listing query that hit `wp_postmeta` with multiple joins and took 340ms drops to 8-15ms with a normalized schema and proper column indexes. This isn't an optimization trick. It's just what happens when prices live in a decimal column instead of a key-value string table.

**Admin speed.** Building your own admin panel with Filament or Nova means it's designed for YOUR data model. No more waiting 8 seconds for the WooCommerce order list to load because it's joining through meta tables on every row.

**Custom business logic.** Need a pricing rule that says "customers who bought Product A in the last 90 days get 15% off Product B"? In WooCommerce, that's a premium plugin or custom WordPress hook spaghetti. In Laravel, it's an Eloquent scope and a middleware.

**Deployment confidence.** No more holding your breath when WordPress pushes a minor update. No more checking if WooCommerce 9.x broke your payment gateway plugin. You control the entire dependency chain through Composer, and you test everything before it goes live.

## What Gets Harder

Honesty goes both ways.

You own everything now. Every tax calculation, every shipping rate lookup, every email notification. When something breaks, there's no plugin support forum to post in. You're the support team. That's the trade-off for total control.

The talent pool shrinks too. Finding a WordPress/WooCommerce developer is easy. Finding a Laravel developer who understands e-commerce patterns takes more effort. If you're building a team around this, factor in hiring difficulty.

Then there's the plugin gap from the other direction. Want to add a loyalty points program? In WooCommerce, you install a plugin and configure it in 20 minutes. In Laravel, you build it yourself, buy a SaaS solution, or find the rare Composer package that fits. Most of the time, you're building.

And the timeline will surprise you. The migration itself takes months for a complex store. You're rebuilding a working system, not building from scratch, which means you're expected to match existing functionality on day one. That's a different kind of pressure than a greenfield project.

## When to Actually Migrate

Not every WooCommerce store needs this. Be honest about whether you're in one of these situations:

**Migrate when:**
- Your `wp_postmeta` table exceeds a few million rows and queries are degrading despite optimization
- You need custom business logic that WooCommerce plugins can't support or only support poorly
- Your team already knows Laravel and maintaining WordPress feels like fighting the framework
- You're planning significant new features that would be easier to build from scratch than to shoehorn into WooCommerce

**Stay on WooCommerce when:**
- Your store handles fewer than 10,000 orders and performance is fine
- Your business requirements are well-served by existing plugins
- You don't have Laravel expertise in-house and don't plan to
- The store is stable, profitable, and doesn't need major new features

The worst reason to migrate is "Laravel is better than WordPress." That's a technology opinion, not a business case. The best reason to migrate is "WooCommerce can no longer support what this business needs to do, and we've exhausted the reasonable alternatives."

## The Migration Checklist Nobody Gives You

If you've decided to move forward, here's the sequence that has worked for me:

1. **Audit your WooCommerce data.** Count products, orders, customers, coupons. Check `wp_postmeta` row count. Identify serialized meta values and custom fields.
2. **Map your plugin dependencies.** List every active plugin. For each one, decide: Composer package, custom build, or third-party API?
3. **Design your Laravel schema first.** Don't just mirror WooCommerce's structure. Design the schema you wish WooCommerce had. Normalize properly. Add the right indexes from day one.
4. **Build the import commands.** Write Artisan commands that read from WordPress and write to Laravel. Run them repeatedly against a copy of production data until the output is clean.
5. **Handle auth migration.** Implement the dual-hash authentication approach so existing customers can log in without resetting passwords.
6. **Build the storefront.** Product pages, cart, checkout, account area. Use Livewire for the interactive bits.
7. **Integrate payment gateways.** Set up Laravel Cashier or direct Stripe/PayPal integration. Test with the migrated customer records.
8. **Set up redirects.** Map every old URL. Set up 301s. Test with a crawler.
9. **Run in parallel.** Keep WooCommerce live while you test the Laravel version against real data. Sync periodically.
10. **Cut over.** Point DNS, submit new sitemap, monitor everything for a week.

Steps 4 and 7 take the longest. Budget accordingly.

I've seen teams underestimate this by 3-4x because they looked at generic guides that treat the migration as a weekend project. It's not. For a store with real traffic, real orders, and real payment processing, plan for weeks to months of parallel development before the cutover.

But when it's done, you have a store that's faster, more maintainable, and built exactly for your business. And you'll never look at `wp_postmeta` the same way again.