private function loadThemeConfig(Config $config, $theme, $childTheme)
 {
     $themePath = $this->app['resources']->getPath('themebase') . '/' . $theme;
     if (file_exists($themePath)) {
         $configFile = $themePath . '/config.yml';
         // insert parent path right after child path.
         $twigPath = $config->get('twigpath');
         if ($twigPath) {
             $childThemePath = $this->app['resources']->getPath('themebase') . '/' . $childTheme;
             $key = array_search($childThemePath, $twigPath);
             if ($key !== false) {
                 array_splice($twigPath, $key, 1, array($childThemePath, $themePath));
             }
             $config->set('twigpath', $twigPath);
         }
         if (file_exists($configFile)) {
             $themeConfig = self::mergeConfigFile($configFile);
             if ($themeConfig) {
                 // load parent theme config, but without overwriting, because child prevails
                 $config->set('theme', Arr::mergeRecursiveDistinct($themeConfig, $config->get('theme')));
                 // multiple levels allowed
                 if (!empty($themeConfig['parent'])) {
                     $this->loadThemeConfig($config, $themeConfig['parent'], $theme);
                 }
             }
         }
     }
 }
Example #2
0
 /**
  * Update packages.
  *
  * @param  $packages array Indexed array of package names to update
  * @param  $options  array [Optional] changed option set
  *
  * @throws \Bolt\Exception\PackageManagerException
  *
  * @return int 0 on success or a positive error code on failure
  */
 public function execute(array $packages = [], array $options = [])
 {
     /** @var $composer \Composer\Composer */
     $composer = $this->getComposer();
     $io = $this->getIO();
     $packageManagerOptions = $this->app['extend.action.options'];
     // Handle passed in options
     if (!empty($options)) {
         $options = Arr::mergeRecursiveDistinct($packageManagerOptions, $options);
     } else {
         $options = $packageManagerOptions;
     }
     $install = Installer::create($io, $composer);
     $config = $composer->getConfig();
     $optimize = $config->get('optimize-autoloader');
     // Set preferred install method
     $prefer = $this->getPreferedTarget($config->get('preferred-install'));
     try {
         $install->setDryRun($options['dryrun'])->setVerbose($options['verbose'])->setPreferSource($prefer['source'])->setPreferDist($prefer['dist'])->setDevMode(!$options['nodev'])->setDumpAutoloader(!$options['noautoloader'])->setRunScripts(!$options['noscripts'])->setOptimizeAutoloader($optimize)->setUpdate(true)->setUpdateWhitelist($packages)->setWhitelistDependencies($options['withdependencies'])->setIgnorePlatformRequirements($options['ignoreplatformreqs'])->setPreferStable($options['preferstable'])->setPreferLowest($options['preferlowest'])->disablePlugins();
         return $install->run();
     } catch (\Exception $e) {
         $msg = __CLASS__ . '::' . __FUNCTION__ . ' recieved an error from Composer: ' . $e->getMessage() . ' in ' . $e->getFile() . '::' . $e->getLine();
         $this->app['logger.system']->critical($msg, ['event' => 'exception', 'exception' => $e]);
         throw new PackageManagerException($e->getMessage(), $e->getCode(), $e);
     }
 }
 /**
  * Constructor.
  *
  * @param array $config
  */
 public function __construct(array $config, $rooturl)
 {
     $this->rooturl = $rooturl;
     $default = $this->getDefaultConfig();
     $this->config = Arr::mergeRecursiveDistinct($default, $config);
     $this->setupProviderConfig();
 }
Example #4
0
 /**
  * Get the return value from the callable.
  *
  * Parameters can be can handled in three ways:
  *   - null              - Nothing passed to the callback
  *   - Indexed array     - Value of each element will be passed to function in order
  *   - Associative array - Key names will attempt to match to the callable function variable names
  */
 protected function invokeCallable(callable $callback, $callbackArguments)
 {
     if ($callbackArguments === null) {
         return call_user_func($callback);
     }
     if (Arr::isIndexedArray($callbackArguments)) {
         return call_user_func_array($callback, (array) $callbackArguments);
     }
     $orderedArgs = $this->getArguments($callback, $callbackArguments);
     return call_user_func_array($callback, $orderedArgs);
 }
Example #5
0
 /**
  * Merge in a yaml file to the config.
  *
  * @param YamlFile $file
  */
 private function addConfig(YamlFile $file)
 {
     $app = $this->getContainer();
     try {
         $newConfig = $file->parse();
     } catch (RuntimeException $e) {
         $app['logger.flash']->error($e->getMessage());
         $app['logger.system']->error($e->getMessage(), ['event' => 'exception', 'exception' => $e]);
         throw $e;
     }
     if (is_array($newConfig)) {
         $this->config = Arr::mergeRecursiveDistinct($this->config, $newConfig);
     }
 }
Example #6
0
 public function testMergeRecusrsiveDistinct()
 {
     $arr1 = array('key' => 'orig value');
     $arr2 = array('key' => 'new value');
     $this->assertEquals(array('key' => 'new value'), Arr::mergeRecursiveDistinct($arr1, $arr2));
     // Needs an exclusion for accept_file_types
     $arr1 = array('accept_file_types' => 'jpg');
     $arr2 = array('accept_file_types' => 'jpg,png');
     Arr::mergeRecursiveDistinct($arr1, $arr2);
     $this->assertEquals(array('accept_file_types' => 'jpg'), $arr1);
     // Test Recusrsion
     $arr1 = array('key' => array('test' => 'new value'));
     $arr2 = array('key' => array('test' => 'nested new value'));
     $this->assertEquals(array('key' => array('test' => 'nested new value')), Arr::mergeRecursiveDistinct($arr1, $arr2));
 }
Example #7
0
 /**
  * Call this in register method.
  *
  * @internal
  */
 protected final function extendRepositoryMapping()
 {
     $app = $this->getContainer();
     $app['storage'] = $app->share($app->extend('storage', function ($entityManager) use($app) {
         foreach ($this->registerRepositoryMappings() as $alias => $map) {
             if (Arr::isIndexedArray($map)) {
                 // Usually caused by [entity, repo] instead of [entity => repo]
                 throw new \RuntimeException(sprintf('Repository mapping for %s `%s` is not an associative array.', __CLASS__, $alias));
             }
             $app['storage.repositories'] += $map;
             $app['storage.metadata']->setDefaultAlias($app['schema.prefix'] . $alias, key($map));
             $entityManager->setRepository(key($map), current($map));
         }
         return $entityManager;
     }));
 }
 /**
  * Build a GET query if required.
  *
  * @param array    $redirect
  * @param FormData $formData
  *
  * @return string
  */
 protected function getRedirectQuery(array $redirect, FormData $formData)
 {
     if (!isset($redirect['query']) || empty($redirect['query'])) {
         return '';
     }
     $query = array();
     if (is_array($redirect['query'])) {
         if (Arr::isIndexedArray($redirect['query'])) {
             foreach ($redirect['query'] as $param) {
                 $query[$param] = $formData->get($param);
             }
         } else {
             foreach ($redirect['query'] as $id => $param) {
                 $query[$id] = $formData->get($param);
             }
         }
     } else {
         $param = $redirect['query'];
         $query[$param] = $formData->get($param);
     }
     return '?' . http_build_query($query);
 }
Example #9
0
 /**
  * merge_with_meta_yml
  */
 private function merge_with_meta_yml()
 {
     $this->metas = Arr::mergeRecursiveDistinct($this->default_settings, $this->parseConfigYaml('meta.yml'));
 }
Example #10
0
 /**
  * Parse a Contenttype's filed and determine the grouping
  *
  * @param array $fields
  * @param array $generalConfig
  *
  * @return array
  */
 protected function parseFieldsAndGroups(array $fields, array $generalConfig)
 {
     $acceptableFileTypes = $generalConfig['accept_file_types'];
     $currentGroup = 'ungrouped';
     $groups = [];
     $hasGroups = false;
     foreach ($fields as $key => $field) {
         unset($fields[$key]);
         $key = str_replace('-', '_', strtolower(Str::makeSafe($key, true)));
         // If field is a "file" type, make sure the 'extensions' are set, and it's an array.
         if ($field['type'] == 'file' || $field['type'] == 'filelist') {
             if (empty($field['extensions'])) {
                 $field['extensions'] = $acceptableFileTypes;
             }
             $field['extensions'] = (array) $field['extensions'];
         }
         // If field is an "image" type, make sure the 'extensions' are set, and it's an array.
         if ($field['type'] == 'image' || $field['type'] == 'imagelist') {
             if (empty($field['extensions'])) {
                 $field['extensions'] = array_intersect(['gif', 'jpg', 'jpeg', 'png'], $acceptableFileTypes);
             }
             $field['extensions'] = (array) $field['extensions'];
         }
         // Make indexed arrays into associative for select fields
         // e.g.: [ 'yes', 'no' ] => { 'yes': 'yes', 'no': 'no' }
         if ($field['type'] === 'select' && isset($field['values']) && is_array($field['values']) && Arr::isIndexedArray($field['values'])) {
             $field['values'] = array_combine($field['values'], $field['values']);
         }
         if (!empty($field['group'])) {
             $hasGroups = true;
         }
         // Make sure we have these keys and every field has a group set.
         $field = array_replace(['class' => '', 'default' => '', 'group' => $currentGroup, 'label' => '', 'variant' => ''], $field);
         // Collect group data for rendering.
         // Make sure that once you started with group all following have that group, too.
         $currentGroup = $field['group'];
         $groups[$currentGroup] = 1;
         $fields[$key] = $field;
         // Repeating fields checks
         if ($field['type'] === 'repeater') {
             $fields[$key] = $this->parseFieldRepeaters($fields, $key);
             if ($fields[$key] === null) {
                 unset($fields[$key]);
             }
         }
     }
     // Make sure the 'uses' of the slug is an array.
     if (isset($fields['slug']) && isset($fields['slug']['uses'])) {
         $fields['slug']['uses'] = (array) $fields['slug']['uses'];
     }
     return [$fields, $hasGroups ? array_keys($groups) : []];
 }
Example #11
0
 /**
  * Update / insert taxonomy for a given content-unit.
  *
  * @param Content $content
  * @param integer $contentId
  * @param array   $taxonomy
  */
 protected function updateTaxonomy($content, $contentId, $taxonomy)
 {
     $contenttype = $content->contenttype;
     $tablename = $this->getTablename('taxonomy');
     $configTaxonomies = $this->app['config']->get('taxonomy');
     // Make sure $contenttypeslug is a 'slug'
     if (is_array($contenttype)) {
         $contenttypeslug = $contenttype['slug'];
     } else {
         $contenttypeslug = $contenttype;
     }
     // If our contenttype has no taxonomies, there's nothing for us to do here.
     if (!isset($contenttype['taxonomy'])) {
         return;
     }
     foreach ($contenttype['taxonomy'] as $taxonomytype) {
         // Set 'newvalues to 'empty array' if not defined
         if (!empty($taxonomy[$taxonomytype])) {
             $newslugs = $this->getIndexedTaxonomy($content, $taxonomy[$taxonomytype]);
         } else {
             $newslugs = array();
         }
         // Get the current values from the DB.
         $query = sprintf("SELECT id, slug, sortorder FROM %s WHERE content_id=? AND contenttype=? AND taxonomytype=?", $tablename);
         $currentvalues = $this->app['db']->executeQuery($query, array($contentId, $contenttypeslug, $taxonomytype), array(\PDO::PARAM_INT, \PDO::PARAM_STR, \PDO::PARAM_STR))->fetchAll();
         if (!empty($currentvalues)) {
             $currentsortorder = $currentvalues[0]['sortorder'];
             $currentvalues = Arr::makeValuePairs($currentvalues, 'id', 'slug');
         } else {
             $currentsortorder = 0;
             $currentvalues = array();
         }
         // Add the ones not yet present.
         foreach ($newslugs as $slug) {
             // If it's like 'desktop#10', split it into value and sortorder.
             list($slug, $sortorder) = explode('#', $slug . "#");
             // @todo clean up and/or refactor
             // If you save this content via anything other than the Bolt
             // backend (see Content->setFromPost), then taxonomies that
             // behave like groupings, will have their sortorders reset to 0.
             if ($configTaxonomies[$taxonomytype]['behaves_like'] == 'grouping' && empty($sortorder) && $sortorder !== '0') {
                 $sortorder = $currentsortorder;
             }
             if (empty($sortorder)) {
                 $sortorder = 0;
             }
             // Make sure we have a 'name'.
             if (isset($configTaxonomies[$taxonomytype]['options'][$slug])) {
                 $name = $configTaxonomies[$taxonomytype]['options'][$slug];
             } else {
                 $name = $slug;
             }
             // Make sure the slug is also set correctly
             if (!isset($configTaxonomies[$taxonomytype]['options'][$slug])) {
                 // Assume we passed a value, instead of a slug. Turn it back into a proper slug
                 if (isset($configTaxonomies[$taxonomytype]['options']) && is_array($configTaxonomies[$taxonomytype]['options']) && array_search($slug, $configTaxonomies[$taxonomytype]['options'])) {
                     $slug = array_search($slug, $configTaxonomies[$taxonomytype]['options']);
                 } else {
                     // make sure it's at least a slug-like value.
                     $slug = $this->app['slugify']->slugify($slug);
                 }
             }
             if ((!in_array($slug, $currentvalues) || $currentsortorder != $sortorder) && !empty($slug)) {
                 // Insert it!
                 $row = array('content_id' => $contentId, 'contenttype' => $contenttypeslug, 'taxonomytype' => $taxonomytype, 'slug' => $slug, 'name' => $name, 'sortorder' => (int) $sortorder);
                 $this->app['db']->insert($tablename, $row);
             }
         }
         // Convert new slugs to lowercase to compare in the delete process
         $newSlugsNormalised = array();
         foreach ($newslugs as $slug) {
             // If it's like 'desktop#10', split it into value and sortorder.
             list($slug, $sortorder) = explode('#', $slug . "#");
             $slug = $this->app['slugify']->slugify($slug);
             if (!empty($sortorder)) {
                 $slug = $slug . '#' . $sortorder;
             }
             $newSlugsNormalised[] = $slug;
         }
         // Delete the ones that have been removed.
         foreach ($currentvalues as $id => $slug) {
             // Make it look like 'desktop#10'
             $valuewithorder = $slug . "#" . $currentsortorder;
             $slugkey = '/' . $configTaxonomies[$taxonomytype]['slug'] . '/' . $slug;
             if (!in_array($slug, $newSlugsNormalised) && !in_array($valuewithorder, $newSlugsNormalised) && !array_key_exists($slugkey, $newSlugsNormalised)) {
                 $this->app['db']->delete($tablename, array('id' => $id));
             }
         }
     }
 }
Example #12
0
 /**
  * @param array $providerConfig
  *
  * @return array
  */
 private function setDefaultConfig(array $providerConfig)
 {
     $default = ['label' => ['sign_in' => null, 'associate' => null], 'keys' => ['client_id' => null, 'client_secret' => null], 'scopes' => null, 'enabled' => false];
     return Arr::mergeRecursiveDistinct($default, $providerConfig);
 }
Example #13
0
 /**
  * If we're using local extensions, install/require the merge plugin.
  *
  * @param array $composerJson
  *
  * @return array
  */
 private function setJsonLocal(array $composerJson)
 {
     $local = $this->app['filesystem']->getFilesystem('extensions')->getDir('local')->exists();
     if ($local === false) {
         return $composerJson;
     }
     $defaults = ['extra' => ['merge-plugin' => ['include' => ['local/*/*/composer.json']]], 'require' => ['wikimedia/composer-merge-plugin' => '^1.3']];
     $composerJson = Arr::mergeRecursiveDistinct($composerJson, $defaults);
     ksort($composerJson);
     return $composerJson;
 }
Example #14
0
 public function testNonArraysAreNotIndexedOrAssociative()
 {
     $this->assertFalse(Arr::isIndexed('derp'));
     $this->assertFalse(Arr::isAssociative('derp'));
 }
Example #15
0
 /**
  * Read and parse the config.yml and config_local.yml configuration files.
  *
  * @return array
  */
 protected function parseGeneral()
 {
     // Read the config and merge it. (note: We use temp variables to prevent
     // "Only variables should be passed by reference")
     $tempconfig = $this->parseConfigYaml('config.yml');
     $tempconfiglocal = $this->parseConfigYaml('config_local.yml');
     $general = Arr::mergeRecursiveDistinct($tempconfig, $tempconfiglocal);
     // Make sure old settings for 'contentsCss' are still picked up correctly
     if (isset($general['wysiwyg']['ck']['contentsCss'])) {
         $general['wysiwyg']['ck']['contentsCss'] = array(1 => $general['wysiwyg']['ck']['contentsCss']);
     }
     // Make sure old settings for 'accept_file_types' are not still picked up. Before 1.5.4 we used to store them
     // as a regex-like string, and we switched to an array. If we find the old style, fall back to the defaults.
     if (isset($general['accept_file_types']) && !is_array($general['accept_file_types'])) {
         unset($general['accept_file_types']);
     }
     // Merge the array with the defaults. Setting the required values that aren't already set.
     $general = Arr::mergeRecursiveDistinct($this->defaultConfig, $general);
     // Make sure the cookie_domain for the sessions is set properly.
     if (empty($general['cookies_domain'])) {
         if (isset($_SERVER['HTTP_HOST'])) {
             $hostname = $_SERVER['HTTP_HOST'];
         } elseif (isset($_SERVER['SERVER_NAME'])) {
             $hostname = $_SERVER['SERVER_NAME'];
         } else {
             $hostname = '';
         }
         // Don't set the domain for a cookie on a "TLD" - like 'localhost', or if the server_name is an IP-address
         if (strpos($hostname, '.') > 0 && preg_match("/[a-z0-9]/i", $hostname)) {
             if (preg_match("/^www[0-9]*./", $hostname)) {
                 $general['cookies_domain'] = '.' . preg_replace("/^www[0-9]*./", '', $hostname);
             } else {
                 $general['cookies_domain'] = '.' . $hostname;
             }
             // Make sure we don't have consecutive '.'-s in the cookies_domain.
             $general['cookies_domain'] = str_replace('..', '.', $general['cookies_domain']);
         } else {
             $general['cookies_domain'] = '';
         }
     }
     // Make sure Bolt's mount point is OK:
     $general['branding']['path'] = '/' . Str::makeSafe($general['branding']['path']);
     $general['database'] = $this->parseDatabase($general['database']);
     return $general;
 }
 /**
  * Build a GET query if required.
  *
  * @param array $redirect
  * @param array $formdata
  */
 private function getRedirectQuery(array $redirect, $formdata)
 {
     $query = array();
     if (Arr::isIndexedArray($redirect['query'])) {
         foreach ($redirect['query'] as $param) {
             $query[$param] = $this->getNormalisedData($formdata[$param]);
         }
     } else {
         $query = $redirect['query'];
     }
     return '?' . http_build_query($query);
 }
Example #17
0
 /**
  * Make a new querystring while preserving current query parameters with the
  * option to override values.
  *
  * @param array $overrides A (key,value)-array with elements to override in
  *                         the current query string.
  * @param bool $buildQuery Returns a querystring if set to true, otherwise
  *                          returns the array with (key,value)-pairs.
  * @return mixed query parameters in either array or string form.
  *
  * @see \Bolt\Helpers\Arr::mergeRecursiveDistinct()
  */
 private function makeQueryParameters($overrides = [], $buildQuery = true)
 {
     $queryParameters = $this->request->query->all();
     // todo: (optional) cleanup. There is a default set of fields we can
     //       expect using this Extension and jsonapi. Or we could ignore
     //       them like we already do.
     // Using Bolt's Helper Arr class for merging and overriding values.
     $queryParameters = Arr::mergeRecursiveDistinct($queryParameters, $overrides);
     $queryParameters = $this->unfixBoltStorageRequest($queryParameters);
     if ($buildQuery) {
         // No need to urlencode these, afaik.
         $querystring = urldecode(http_build_query($queryParameters));
         if (!empty($querystring)) {
             $querystring = '?' . $querystring;
         }
         return $querystring;
     }
     return $queryParameters;
 }
Example #18
0
 /**
  * Load and process a give config file.
  *
  * @param string $configfile Fully qualified file path
  */
 private function loadConfigFile($configfile)
 {
     $yamlparser = new Yaml\Parser();
     $newConfig = $yamlparser->parse(file_get_contents($configfile) . "\n");
     // Don't error on empty config files
     if (is_array($newConfig)) {
         $this->config = Arr::mergeRecursiveDistinct($this->config, $newConfig);
     }
 }
Example #19
0
 /**
  * Insert an individual Contenttype record into the database
  *
  * @param string $filename
  * @param string $contenttypeslug
  * @param array  $values
  *
  * @return boolean
  */
 private function insertRecord($filename, $contenttypeslug, array $values)
 {
     // Determine a/the slug
     $slug = isset($values['slug']) ? $values['slug'] : substr($this->app['slugify']->slugify($values['title']), 0, 127);
     if (!$this->isRecordUnique($contenttypeslug, $slug)) {
         $this->setWarning(true)->setWarningMessage("File '{$filename}' has an exiting Contenttype '{$contenttypeslug}' with the slug '{$slug}'! Skipping record.");
         return false;
     }
     // Get a status
     if (isset($values['status'])) {
         $status = $values['status'];
     } else {
         $status = $this->contenttypes[$contenttypeslug]['default_status'];
     }
     // Transform the 'publish' action to a 'published' status
     $status = $status === 'publish' ? 'published' : $status;
     // Insist on a title field
     if (!isset($values['title'])) {
         $this->setWarning(true)->setWarningMessage("File '{$filename}' has a '{$contenttypeslug}' with a missing title field! Skipping record.");
         return false;
     }
     // Set up default meta
     $meta = ['slug' => $slug, 'datecreated' => date('Y-m-d H:i:s'), 'datepublish' => $status == 'published' ? date('Y-m-d H:i:s') : null, 'ownerid' => 1];
     $values = Arr::mergeRecursiveDistinct($values, $meta);
     $record = $this->app['storage']->getEmptyContent($contenttypeslug);
     $record->setValues($values);
     if ($this->app['storage']->saveContent($record) === false) {
         $this->setWarning(true)->setWarningMessage("Failed to imported record with title: {$values['title']} from '{$filename}'! Skipping record.");
         return false;
     } else {
         $this->setNotice(true)->setNoticeMessage("Imported record with title: {$values['title']}.");
         return true;
     }
 }
Example #20
0
 /**
  * Build an OR group that is added to the AND.
  *
  * @param QueryBuilder $qb
  * @param string       $parentColumnName
  * @param array        $options
  *
  * @return CompositeExpression
  */
 protected function buildWhereOr(QueryBuilder $qb, $parentColumnName, array $options)
 {
     $orX = $qb->expr()->orX();
     foreach ($options as $columnName => $option) {
         if (empty($options[$columnName])) {
             continue;
         } elseif (Arr::isIndexedArray($options)) {
             $key = $parentColumnName . '_' . $columnName;
             $orX->add("{$parentColumnName} = :{$key}");
             $qb->setParameter($key, $option);
         } else {
             $orX->add("{$columnName} = :{$columnName}");
             $qb->setParameter($columnName, $option);
         }
     }
     return $orX;
 }
Example #21
0
 /**
  * Enforce the default JSON settings.
  *
  * @param array $json
  *
  * @return array
  */
 private function setJsonDefaults(array $json)
 {
     $rootPath = $this->app['resources']->getPath('root');
     $extensionsPath = $this->app['resources']->getPath('extensions');
     $srcPath = $this->app['resources']->getPath('src');
     $webPath = $this->app['resources']->getPath('web');
     $pathToRoot = Path::makeRelative($rootPath, $extensionsPath);
     $pathToWeb = Path::makeRelative($webPath, $extensionsPath);
     $eventPath = Path::makeRelative($srcPath . '/Composer/EventListener', $extensionsPath);
     /** @deprecated Handle BC on 'stability' key until 4.0 */
     $minimumStability = $this->app['config']->get('general/extensions/stability') ?: $this->app['config']->get('general/extensions/composer/minimum-stability', 'stable');
     // Enforce standard settings
     $defaults = ['name' => 'bolt/extensions', 'description' => 'Bolt extension installation interface', 'license' => 'MIT', 'repositories' => ['packagist' => false, 'bolt' => ['type' => 'composer', 'url' => $this->app['extend.site'] . 'satis/']], 'minimum-stability' => $minimumStability, 'prefer-stable' => true, 'config' => ['discard-changes' => true, 'preferred-install' => 'dist'], 'provide' => ['bolt/bolt' => Bolt\Version::forComposer()], 'extra' => ['bolt-web-path' => $pathToWeb, 'bolt-root-path' => $pathToRoot], 'autoload' => ['psr-4' => ['Bolt\\Composer\\EventListener\\' => $eventPath]], 'scripts' => ['post-autoload-dump' => 'Bolt\\Composer\\EventListener\\PackageEventListener::dump', 'post-package-install' => 'Bolt\\Composer\\EventListener\\PackageEventListener::handle', 'post-package-update' => 'Bolt\\Composer\\EventListener\\PackageEventListener::handle']];
     $json = Arr::replaceRecursive($json, $defaults);
     ksort($json);
     return $json;
 }