Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Wordpress Importer Laravel Package

wordpress/wordpress-importer

Official WordPress plugin for importing content from a WXR export file. Migrates posts, pages, comments, custom fields, categories, tags, and media attachments, making it easy to move or merge content between WordPress sites.

Deep Wiki
Context7

Getting Started

Reality check first: wordpress/wordpress-importer is not a Laravel package — it is the official WordPress Importer plugin. There is no service provider, no config file, no facade, and no namespaced autoloading. Everything lives in global scope (WP_Import, WXR_Parser) and calls WordPress core functions (wp_insert_post(), wp_set_object_terms(), media_handle_sideload()). Deciding how you host it is the whole design decision.

Three viable modes, pick one before writing code:

  1. Sidecar WordPress (recommended) — a WP install that shares your DB or is read back over REST/Corcel. Laravel orchestrates imports via WP-CLI.
  2. In-process bootstrap — an Artisan command that requires wp-load.php and calls WP_Import directly. Fast to build, painful to operate.
  3. Parser-only — use WXR_Parser to read the XML, then map to your own Eloquent models. Usually the right answer if the content isn't staying in WordPress.

Minimal steps:

# as a plugin (composer/installers with type=wordpress-plugin, or just drop it in)
composer require wordpress/wordpress-importer
# or: git clone into wp-content/plugins/wordpress-importer
wp plugin activate wordpress-importer

First use case: export from the legacy site (Tools → Export → All content), then import into staging and diff posts/media/menus:

wp import legacy-export.xml --authors=create --path=/var/www/wordpress

Where to look first: the plugin bootstrap (wordpress-importer.php, which registers the importer and loads the classes) and class-wp-import.php — that file is the product. Read import() top to bottom; it is the entire pipeline: import_start() → process_terms() → process_posts() → backfill_parents() → backfill_attachment_urls() → import_end(). The class-wxr-parser*.php files are the interchangeable XML readers (SimpleXML → XMLReader/XML → regex fallback for malformed exports).

Implementation Patterns

1. Orchestrator: Laravel queues, WP-CLI does the work

Best isolation. No WordPress globals leak into your app, no second DB connection, and a hung import kills a child process instead of your worker.

// app/Jobs/ImportWordPressExport.php
public function handle(): void
{
    $result = Process::path(config('wp.path'))
        ->timeout(1800)
        ->run([
            'wp', 'import', $this->xmlPath,
            '--authors=create',
            '--skip=image_resize',   // check `wp help import` for your WP-CLI version
            '--path=' . config('wp.path'),
        ]);

    $result->throw();

    SearchReplaceJob::dispatch($this->sourceUrl, $this->targetUrl);
}

Follow with wp search-replace 'https://old.tld' 'https://new.tld' --all-tables — the importer rewrites attachment URLs in post bodies but not hardcoded links in options, widgets, or serialized builder data.

2. In-process bootstrap (only for small, controlled imports)

define('WP_LOAD_IMPORTERS', true);
define('WP_IMPORTING', true);
require config('wp.path') . '/wp-load.php';
require_once ABSPATH . 'wp-admin/includes/import.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';

$importer = new WP_Import();
$importer->fetch_attachments = false;   // do media out-of-band
$importer->import($absoluteXmlPath);

Run this only from an Artisan command in a dedicated queue, never from a web request. Verify import() exists in your pinned version — the 0.8 rewrite reworked the UI layer and method surface.

3. Parser-only: WXR → Eloquent

WXR_Parser is the least WordPress-coupled class in the package, but it returns WP_Error, so you need brain/monkey or a tiny shim. Then map into your own models:

$parser = new WXR_Parser();
$data   = $parser->parse($path);            // ['posts' => [...], 'categories' => [...], ...]

foreach ($data['posts'] as $post) {
    if ($post['post_type'] !== 'post' || $post['status'] !== 'publish') continue;

    Article::updateOrCreate(
        ['legacy_id' => $post['post_id']],
        [
            'title'   => $post['post_title'],
            'slug'    => $post['post_name'],
            'body'    => $post['post_content'],
            'published_at' => $post['post_date'],
            'meta'    => collect($post['postmeta'])->pluck('value', 'key')->all(),
        ],
    );
}

This gives you full control over media, taxonomy mapping, and re-runs — at the cost of reimplementing attachment and menu logic.

4. Hook-based customization (in-process / sidecar)

The importer exposes filters that solve most "it did the wrong thing" problems:

// Idempotency: match on a legacy ID stored in post meta instead of title+date+content
add_filter('wp_import_existing_post', function ($post_id, $post) {
    $found = get_posts([
        'meta_key' => '_legacy_wxr_id', 'meta_value' => $post['post_id'],
        'post_type' => $post['post_type'], 'fields' => 'ids', 'limit' => 1,
    ]);
    return $found[0] ?? 0;
}, 10, 2);

// Tag every imported post so you can find/roll back later
add_filter('wp_import_post_data_processed', function ($postdata, $post) {
    $postdata['meta_input']['_legacy_wxr_id'] = $post['post_id'];
    $postdata['meta_input']['_import_batch']   = getenv('IMPORT_BATCH');
    return $postdata;
}, 10, 2);

add_filter('wp_import_post_meta', fn ($meta, $post_id, $post) => array_filter(
    $meta, fn ($m) => ! str_starts_with($m['key'], '_oembed_')
), 10, 3);

import_start / import_end are your timing and logging hooks; wp_import_post_terms and wp_import_terms cover taxonomy remapping.

5. Keep a mapping ledger in Laravel

Whatever mode you choose, persist source_id → local_id (and source_url → attachment_id) in a table. It makes re-runs idempotent, lets you resume after a timeout, and is the only reliable way to fix up relationships inside serialized meta later.

Gotchas and Tips

  • Not a library. Classes are global and unloaded unless you require the plugin bootstrap. Composer's autoloader alone will not make new WP_Import() work. Don't require it inside a Laravel request — WordPress calls exit(), installs its own error handler, and opens a second MySQL connection to the same database (deadlock risk with your open transactions).
  • The whole XML is in memory. WXR_Parser_SimpleXML loads the full document; large exports blow memory_limit or hit libxml limits. Split exports by date range/author (WP-CLI wp export --post_type=post --start_date=…) and run several imports rather than raising memory.
  • Attachments are synchronous. fetch_attachments = true downloads every remote file mid-import — slow, timeout-prone, and it silently skips files behind hotlink protection or expired URLs. Prefer fetch_attachments = false, then sync media separately (or wp media import) and rewrite URLs afterwards.
  • Silent duplicate skipping. post_exists() matches on title + date + content, so a re-import of edited content creates duplicates while a new post that happens to collide gets skipped. Always override with wp_import_existing_post.
  • IDs are not remapped inside serialized data. Elementor _elementor_data, ACF relationship fields, and page-builder JSON keep the old post/attachment IDs. Backfill only handles attachment URLs and menu parents. Plan a post-import fixup pass keyed off your mapping table.
  • Users: the importer creates accounts with random passwords and default role; existing emails are matched and reused rather than overwritten. Imported authors cannot log in until they reset. Map authors explicitly for anything beyond a one-off.
  • Dates are inserted as site-local wall-clock time, unconverted. Importing into a site with a different timezone shifts every timestamp.
  • WP_IMPORTING is defined during import. Plugins that check it (notification emails, cache warmers, indexers) will go quiet — useful, but it also means your post-import verification can see stale state until you flush caches.
  • Kses and shortcodes apply. Content is inserted through normal post APIs, so unfiltered_html capability and content filters can alter markup, and imported shortcodes/blocks execute on render. Treat any WXR from an untrusted source as untrusted PHP/HTML input; validate the XML source and disable external entity loading in your PHP config.
  • Pin the version. 0.8 is a substantial rewrite of the UI and the import flow relative to 0.7.x. Internal method names and behavior are not a stable API — lock a version and re-test on upgrade.
  • Testing: build a 3-post fixture WXR (one post with a child page, one attachment, one menu) and assert two things on every run: the import is idempotent, and your mapping ledger has no orphans.
  • License: GPL-2.0. Internal/sidecar use is unproblematic; redistributing it bundled inside a proprietary product attaches GPL obligations to that component.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums