Created
April 25, 2025 05:44
-
-
Save sergeytolkachyov/ecfa5aef518711b6f6dff868ff2bbc3e to your computer and use it in GitHub Desktop.
Export articles from Joomla 3 to Joomla 5 via REST API. Joomla 3 CLI script.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @package Joomla.Cli | |
* | |
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org> | |
* @license GNU General Public License version 2 or later; see LICENSE.txt | |
*/ | |
/** | |
* This is a CRON script which should be called from the command-line, not the | |
* web. For example something like: | |
* | |
* /usr/bin/php /path/to/joomla/cli/export_articles.php --context=articles | |
* | |
* First, you need to create all the necessary fields and categories | |
* in the new Joomla in order to record their IDs and aliases in the correspondence maps. | |
* | |
* You can adapt this script to transfer not only Joomla articles, | |
* but also other entities: categories, contacts, banners, etc. | |
* | |
* @Author Sergey Tolkachyov | |
* @link https://web-tolk.ru | |
*/ | |
// Set flag that this is a parent file. | |
use Joomla\CMS\Http\Http; | |
use Joomla\CMS\Input\Cli; | |
use Joomla\Registry\Registry; | |
use Joomla\CMS\Http\HttpFactory; | |
use Joomla\CMS\Uri\Uri; | |
const _JEXEC = 1; | |
error_reporting(E_ALL | E_NOTICE); | |
ini_set('display_errors', 1); | |
// Load system defines | |
if (file_exists(dirname(__DIR__) . '/defines.php')) | |
{ | |
require_once dirname(__DIR__) . '/defines.php'; | |
} | |
if (!defined('_JDEFINES')) | |
{ | |
define('JPATH_BASE', dirname(__DIR__)); | |
require_once JPATH_BASE . '/includes/defines.php'; | |
} | |
require_once JPATH_LIBRARIES . '/import.legacy.php'; | |
require_once JPATH_LIBRARIES . '/cms.php'; | |
// Load the configuration | |
require_once JPATH_CONFIGURATION . '/configuration.php'; | |
// System configuration. | |
$config = new JConfig; | |
define('JDEBUG', $config->debug); | |
// Register FieldsHelper | |
JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php'); | |
/** | |
* This script will fetch the update information for all extensions and store | |
* them in the database, speeding up your administrator. | |
* | |
* @since 2.5 | |
*/ | |
class ExportArticles extends JApplicationCli | |
{ | |
/** | |
* Joomla 3 category ids for handling, | |
* from which we process articles | |
* | |
* @var int[] | |
* @since 1.0.0 | |
*/ | |
public static $catids = [ | |
21, // Category 1 | |
585, // Category 2 etc. | |
]; | |
/** | |
* A list of available commands for the script. | |
* Not used yet | |
* | |
* @var int[] | |
* @since 1.0.0 | |
*/ | |
public $validParams = [ | |
'context', | |
]; | |
/** | |
* Valid values for command `context` | |
* | |
* @var string[] | |
* @since 1.0.0 | |
*/ | |
public $validContext = [ | |
'articles', | |
'categories', | |
]; | |
/** | |
* @var array|mixed | |
* @since 1.0.0 | |
*/ | |
public $commands = []; | |
/** | |
* Joomla 5+ API URL. Where do we copy articles to. | |
* | |
* @var string | |
* @since 1.0.0 | |
*/ | |
private $apiUrl = 'https://your-joomla-5.ru'; | |
/** | |
* Joomla 5+ API token. We are looking in the user's profile, | |
* from which we will create articles on the receiving site. | |
* | |
* @var string | |
* @since 1.0.0 | |
*/ | |
private $apiToken = 'c2hhMjU2OjI3MTo4NTY3MzhhN2M4NDZhMTIyZ********yZjBjM2Y0YmJiNzIxOWM5MWU0NTczOGQ3NTY2MmZh'; | |
/** | |
* Joomla HTTP-client for HTTP requests | |
* | |
* @var Http | |
* @since 1.0.0 | |
*/ | |
private $http; | |
/** | |
* Item list limit | |
* | |
* @var int | |
* @since 1.0.0 | |
*/ | |
public static $list_limit = 100000; | |
/** | |
* A map of matching old to new data. | |
* The array key is an old property from Joomla 3, | |
* The value is a new property in Joomla 5. | |
* | |
* @var array | |
* @since 1.0.0 | |
*/ | |
private $itemPropertiesMap = [ | |
'title' => 'title', | |
'alias' => 'alias', | |
'introtext' => 'introtext', | |
'fulltext' => 'fulltext', | |
'state' => 'state', | |
'catid' => 'catid', | |
'created' => 'created', | |
'modified' => 'modified', | |
'publish_up' => 'publish_up', | |
'images' => 'images', | |
'urls' => 'urls', | |
'attribs' => 'attribs', | |
'ordering' => 'ordering', | |
'metakey' => 'metakey', | |
'metadesc' => 'metadesc', | |
'hits' => 'hits', | |
'metadata' => 'metadata', | |
'featured' => 'featured', | |
'language' => 'language', | |
'note' => 'note', | |
]; | |
/** | |
* A map of how the old categories match the new ones. | |
* The array key is the id of the old category in Joomla 3 | |
* The value of the array is the id of the new category in Joomla 5 | |
* | |
* @var array | |
* @since 1.0.0 | |
*/ | |
private $categoriesMapOldToNew = [ | |
21 => 8, // Joomla 3 catid => Joomla 5 catid | |
585 => 9, | |
108 => 10, | |
111 => 11, | |
114 => 12, | |
]; | |
/** | |
* A map of how the CUSTOM FIELDS match the old and new ONES. | |
* The array key is an old system name (alias) field in Joomla 3 | |
* The array value is the system name of the new field in Joomla 5 | |
* | |
* @var array | |
* @since 1.0.0 | |
*/ | |
private $customFieldsMapOldToNew = [ | |
'photo' => 'photo', | |
'koordinaty' => 'coords', | |
'opisanie-rezhima-raboty' => 'rezhim-raboty', | |
'text-oplata' => 'forma-oplaty', | |
]; | |
/** | |
* JDatabase | |
* @var $db | |
* @since 1.0.0 | |
*/ | |
private $db; | |
public function __construct(Cli $input = null, Registry $config = null, JEventDispatcher $dispatcher = null) | |
{ | |
parent::__construct($input, $config, $dispatcher); | |
$this->commands = $this->input->getArray(); | |
$this->http = HttpFactory::getHttp(null, ['curl', 'stream']); | |
$this->apiUrl = Uri::getInstance($this->apiUrl); | |
$this->db = JFactory::getDbo(); | |
} | |
/** | |
* Entry point for the script | |
* | |
* @return void | |
* | |
* @since 2.5 | |
*/ | |
public function doExecute() | |
{ | |
// are the parameters of the receiving site specified | |
if (!$this->checkCredentials()) | |
{ | |
return; | |
} | |
// are there any commands for the script | |
// and are they correct | |
if (!$this->checkCommands()) | |
{ | |
return; | |
} | |
$context = $this->commands['context']; | |
switch ($context) | |
{ | |
case 'articles': // You can add your own cases here with custom methods | |
default : | |
$this->copyArticles(); | |
break; | |
} | |
$this->out('Finished'); | |
} | |
/** | |
* Checking if the API URL and API TOKEN are filled in | |
* | |
* @return bool | |
* @since 1.0.0 | |
*/ | |
private function checkCredentials() | |
{ | |
if (empty($this->apiUrl)) | |
{ | |
$this->out( | |
'Error: $apiUrl is empty! You did not specify the api url. Fill it in with the value of the $apiUrl variable.' | |
); | |
return false; | |
} | |
if (empty($this->apiToken)) | |
{ | |
$this->out( | |
'Error: $apiToken is empty! You did not specify the api url. Fill it in with the value of the $apiToken variable.' | |
); | |
return false; | |
} | |
return true; | |
} | |
/** | |
* We check the commands from the command line. | |
* | |
* @return bool | |
* @since 1.0.0 | |
*/ | |
private function checkCommands() | |
{ | |
if (empty($this->commands)) | |
{ | |
$this->out("You did not specify any commands. For example, --context=articles"); | |
return false; | |
} | |
if (!array_key_exists('context', $this->commands)) | |
{ | |
$this->out( | |
"You did not specify the --context command. Specify it, please. For example, --context=articles" | |
); | |
return false; | |
} | |
$context = $this->commands['context']; | |
if (!in_array($context, $this->validContext)) | |
{ | |
$this->out("You have specified wrong context: " . $context); | |
$this->out('The list of valid contextes are: ' . implode(', ', $this->validContext)); | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Create articles in the new Joomla 5 | |
* via the REST API | |
* | |
* @throws Exception | |
* @since 1.0.0 | |
*/ | |
public function copyArticles() | |
{ | |
$items = $this->getArticlesList(); | |
if(empty($items)) | |
{ | |
$this->out('Error: There is no articles found'); | |
return; | |
} | |
$this->apiUrl->setPath('/api/index.php/v1/content/articles'); | |
foreach ($items as $item) | |
{ | |
$data = []; | |
foreach ($item as $oldKey => $oldValue) | |
{ | |
if(array_key_exists($oldKey, $this->itemPropertiesMap)) | |
{ | |
$newKey = $this->itemPropertiesMap[$oldKey]; | |
// Set new id for category - by Joomla 5. | |
if($newKey == 'catid') | |
{ | |
$oldValue = !empty($this->categoriesMapOldToNew[$oldValue]) ? $this->categoriesMapOldToNew[$oldValue] : $oldValue; | |
$oldContentCatId = $oldValue; | |
} | |
$data[$newKey] = $oldValue; | |
} | |
} | |
// Get the custom fields for article | |
$fields = $this->getCustomFields('com_content.article', $item); | |
if(!empty($fields)) | |
{ | |
$data['com_fields'] = []; | |
$nerudasId = -1; | |
foreach ($fields as $field) | |
{ | |
// Custom changes for field START | |
if($field['name'] == 'field-name-old' && !empty($field['value'])) | |
{ | |
$url = new Uri(trim($field['value'])); | |
$anyId = $url->getVar('map')['active_id']; | |
$data['com_fields']['field-name-new'] = $anyId; | |
} | |
// Custom changes for field END | |
// If there is a name of the old field in the mapping map | |
if(array_key_exists($field['name'], $this->customFieldsMapOldToNew)) | |
{ | |
$newFieldName = $this->customFieldsMapOldToNew[$field['name']]; | |
// FOR MEDIA FIELDS - USE OTHER SCTRUCTURE | |
if(in_array($newFieldName, [ | |
'photo', | |
'skhema-proezda' | |
])) | |
{ | |
$data['com_fields'][$newFieldName]['imagefile'] = trim($field['value']); | |
} else { | |
$data['com_fields'][$newFieldName] = trim($field['value']); | |
} | |
} | |
} | |
} | |
/** | |
* Additional custom changes START | |
*/ | |
// Write your custom code here | |
/** | |
* Additional custom changes END | |
*/ | |
// Send request to Joomla 5 REST API | |
if(!empty($data)) | |
{ | |
$result = $this->sendRequest($this->apiUrl->toString(), $data); | |
if($result->code !== 200) | |
{ | |
$errors = []; | |
if(!empty($result->body)) | |
{ | |
$body = json_decode($result->body); | |
$errors[] = [ | |
'title' => 'Creation error', | |
'item_title' => $data['title'], | |
'error_code' => $result->code, | |
'errors' => $body->errors, | |
]; | |
} | |
if(!empty($errors)) | |
{ | |
// Log the errors to errors.txt | |
file_put_contents(__DIR__.'/errors.txt', print_r($errors, true).PHP_EOL, FILE_APPEND); | |
} | |
} else { | |
$this->out('Item '.$data['title'].' successfully transfered'); | |
} | |
} | |
} | |
} | |
/** | |
* Get the articles list | |
* | |
* @throws Exception | |
* @since 1.0.0 | |
*/ | |
public function getArticlesList() | |
{ | |
$items = []; | |
if(empty(self::$catids)) | |
{ | |
$this->out('There are no source category ids specified in $catids parameter'); | |
return $items; | |
} | |
$query = $this->db->getQuery(true); | |
$query->select(['*']) | |
->from('#__content') | |
->where($this->db->quoteName('catid') . ' IN (' . implode(',', self::$catids) . ')') | |
->setLimit(self::$list_limit); | |
try | |
{ | |
$items = $this->db->setQuery($query)->loadObjectList(); | |
$this->out('Articles list: '.count($items).' articles found in categories id: '.implode(', ', self::$catids)); | |
} | |
catch (Exception $exception) | |
{ | |
$this->out('Error when trying to get a list of articles from the database.'); | |
$this->out($exception->getCode().' - '.$exception->getMessage()); | |
} | |
return $items; | |
} | |
/** | |
* @param string $context 'com_content.article' etc. | |
* @param array $item | |
* | |
* @return array | |
* | |
* @since 1.0.0 | |
*/ | |
public function getCustomFields($context, $item) | |
{ | |
$db = $this->db; | |
$query = $db->getQuery(true); | |
$query->select( | |
[ | |
'a.id', | |
'a.name', | |
'fv.value', | |
] | |
) | |
->from($db->quoteName('#__fields', 'a')) | |
->where($db->quoteName('a.context'). ' = ' . $db->quote($context)) | |
->innerJoin('#__fields_values AS fv ON a.id = fv.field_id') | |
->where($db->quoteName('fv.item_id'). ' = ' . $db->quote($item->id)); | |
$fields = $db->setQuery($query)->loadAssocList(); | |
return $fields; | |
} | |
/** | |
* @param string $url | |
* @param array $data | |
* | |
* | |
* @since 1.0.0 | |
*/ | |
private function sendRequest($url, $data) | |
{ | |
$data = (new Joomla\Registry\Registry($data))->toString(); | |
$headers = [ | |
'Authorization' => 'Bearer ' . $this->apiToken, | |
'Content-Type' => 'application/json', | |
]; | |
// $data = (new Joomla\Registry\Registry($data))->toString(); | |
$response = $this->http->post($this->apiUrl, $data, $headers); | |
return $response; | |
} | |
} | |
JApplicationCli::getInstance('ExportArticles')->execute(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment