/** * Stylize content using pre-defined style. * * @param string $element * @param string $type * @param string $context * @return string */ public function style($element, $type, $context = '') { if (!empty($style = $this->getStyle($type, $context))) { return \Spiral\interpolate($this->element, compact('style', 'element')); } return $element; }
/** * @param AbstractTable $table * @param RecordSchema $record */ public function __construct(AbstractTable $table, RecordSchema $record) { $altered = []; foreach ($table->alteredColumns() as $column) { $altered[] = $column->getName(); } parent::__construct(\Spiral\interpolate('Passive table "{database}"."{table}" ({record}), were altered, columns: {columns}', ['database' => $record->getDatabase(), 'table' => $table->getName(), 'record' => $record, 'columns' => join(', ', $altered)])); }
/** * Handle log message. * * @param int $level Log message level. * @param string $message Message. * @param array $context Context data. */ public function __invoke($level, $message, array $context = []) { $message = \Spiral\interpolate($this->options['format'], ['date' => date($this->options['dateFormat'], time()), 'level' => $level, 'message' => $message]); if ($this->files->append($this->options['filename'], "{$message}\n", $this->options['mode'], true)) { if ($this->files->size($this->options['filename']) > $this->options['filesize']) { $this->files->move($this->options['filename'], $this->options['filename'] . $this->options['rotatePostfix']); } } }
/** * {@inheritdoc} */ public function log($level, $message, array $context = []) { if (!array_key_exists($level, $this->formats)) { return; } $string = \Spiral\interpolate($message, $context); if (!empty($this->formats[$level])) { //Formatting string $string = \Spiral\interpolate('<{format}>{string}</{format}>', ['string' => $string, 'format' => $this->formats[$level]]); } if (!empty($this->section) && !empty($this->formatter)) { $string = $this->formatter->formatSection($this->section, $string); } $this->output->writeln($string); }
/** * Helper method used to interpolate SQL query with set of parameters, must be used only for * development purposes and never for real query. * * @param string $query * @param array $parameters Parameters to be binded into query. * @return mixed */ public static function interpolate($query, array $parameters = []) { if (empty($parameters)) { return $query; } //Flattening first $parameters = self::flattenParameters($parameters); //Let's prepare values so they looks better foreach ($parameters as &$parameter) { $parameter = self::prepareParameter($parameter); unset($parameter); } reset($parameters); if (!is_int(key($parameters))) { //Associative array return \Spiral\interpolate($query, $parameters, '', ''); } foreach ($parameters as $parameter) { $query = preg_replace('/\\?/', $parameter, $query, 1); } return $query; }
/** * {@inheritdoc} * * Default symfony pluralizer to be used. Parameters will be embedded into string using { and } * braces. In addition you can use forced parameter {n} which contain formatted number value. * * @throws LocaleException */ public function transChoice($id, $number, array $parameters = [], $domain = self::DEFAULT_DOMAIN, $locale = null) { if (empty($parameters['{n}'])) { $parameters['{n}'] = number_format($number); } //Automatically falls back to default locale $translation = $this->get($domain, $id, $locale); try { $pluralized = $this->selector->choose($translation, $number, $locale); } catch (\InvalidArgumentException $exception) { //Wrapping into more explanatory exception throw new PluralizationException($exception->getMessage(), $exception->getCode(), $exception); } return \Spiral\interpolate($pluralized, $parameters); }
/** * Highlight one line. * * @param int $number * @param string $code * @param bool $highlighted * @return string */ public function line($number, $code, $highlighted = false) { return \Spiral\interpolate($this->templates[$highlighted ? 'highlighted' : 'line'], compact('number', 'code')); }
/** * Copy table data to another location. * * @see http://stackoverflow.com/questions/4007014/alter-column-in-sqlite * @param AbstractTable $temporary * @param array $mapping Association between old and new columns (quoted). */ private function copyData(AbstractTable $temporary, array $mapping) { $this->logger()->debug("Copying table data from {source} to {table} using mapping ({columns}) => ({target}).", ['source' => $this->driver->identifier($this->initial->getName()), 'table' => $temporary->getName(true), 'columns' => join(', ', $mapping), 'target' => join(', ', array_keys($mapping))]); $query = \Spiral\interpolate("INSERT INTO {table} ({target}) SELECT {columns} FROM {source}", ['source' => $this->driver->identifier($this->initial->getName()), 'table' => $temporary->getName(true), 'columns' => join(', ', $mapping), 'target' => join(', ', array_keys($mapping))]); //Let's go $this->driver->statement($query); }
/** * Format string using previously named arguments from values array. Arguments that are not found * will be skipped without any notification. Extra arguments will be skipped as well. * * Example: * Hello [:name]! Good [:time]! * + array('name'=>'Member','time'=>'day') * * Output: * Hello Member! Good Day! * * @param string $format Formatted string. * @param array $values Arguments (key=>value). Will skip n * @param string $prefix Value prefix, "{" by default. * @param string $postfix Value postfix "}" by default. * @return mixed */ function interpolate($format, array $values, $prefix = '{', $postfix = '}') { return \Spiral\interpolate($format, $values, $prefix, $postfix); }
/** * {@inheritdoc} */ protected function doColumnChange(AbstractColumn $column, AbstractColumn $dbColumn) { /** * @var ColumnSchema $column */ //Rename is separate operation if ($column->getName() != $dbColumn->getName()) { $this->driver->statement(\Spiral\interpolate('ALTER TABLE {table} RENAME COLUMN {original} TO {column}', ['table' => $this->getName(true), 'column' => $column->getName(true), 'original' => $dbColumn->getName(true)])); $column->setName($dbColumn->getName()); } //Postgres columns should be altered using set of operations if (!($operations = $column->alterOperations($dbColumn))) { return; } //Postgres columns should be altered using set of operations $query = \Spiral\interpolate('ALTER TABLE {table} {operations}', ['table' => $this->getName(true), 'operations' => trim(join(', ', $operations), ', ')]); $this->driver->statement($query); }
/** * {@inheritdoc} */ protected function doIndexChange(AbstractIndex $index, AbstractIndex $dbIndex) { $query = \Spiral\interpolate("ALTER TABLE {table} DROP INDEX {original}, ADD {statement}", ['table' => $this->getName(true), 'original' => $dbIndex->getName(true), 'statement' => $index->sqlStatement(false)]); $this->driver->statement($query); }
/** * {@inheritdoc} */ public function uri($parameters = []) { if (empty($this->compiled)) { $this->compile(); } $parameters = array_merge($this->compiled['options'], $this->defaults, $this->matches, $this->fetchSegments($parameters, $query)); //Uri without empty blocks (pretty stupid implementation) $path = strtr(\Spiral\interpolate($this->compiled['template'], $parameters, '<', '>'), ['[]' => '', '[/]' => '', '[' => '', ']' => '', '//' => '/']); $uri = new Uri(($this->withHost ? '' : $this->prefix) . rtrim($path, '/')); return empty($query) ? $uri : $uri->withQuery(http_build_query($query)); }
/** * Will specify missing fields in relation definition using default definition options. Such * options are dynamic and populated based on values fetched from related records. */ protected function clarifyDefinition() { foreach ($this->defaultDefinition as $property => $pattern) { if (isset($this->definition[$property])) { //Specified by user continue; } if (!is_string($pattern)) { //Some options are actually array of options $this->definition[$property] = $pattern; continue; } //Let's create option value using default proposer values $this->definition[$property] = \Spiral\interpolate($pattern, $this->proposedDefinitions()); } }
/** * Payloads of every message raised by spiral Logger. * * @return array */ public function globalMessages() { $result = []; foreach ($this->globalLog as $message) { //Delayed interpolating $message[Logger::MESSAGE_BODY] = \Spiral\interpolate($message[Logger::MESSAGE_BODY], $message[Logger::MESSAGE_CONTEXT]); $result[] = $message; } return $result; }
/** * {@inheritdoc} */ public function createUri($parameters = [], $basePath = '/', SlugifyInterface $slugify = null) { if (empty($this->compiled)) { $this->compile(); } $parameters = array_map([!empty($slugify) ? $slugify : new Slugify(), 'slugify'], $parameters + $this->defaults + $this->compiled['options']); //Uri without empty blocks $uri = strtr(\Spiral\interpolate($this->compiled['template'], $parameters, '<', '>'), ['[]' => '', '[/]' => '', '[' => '', ']' => '', '//' => '/']); $uri = new Uri(($this->withHost ? '' : $basePath) . $uri); //Getting additional query parameters if (!empty($queryParameters = array_diff_key($parameters, $this->compiled['options']))) { $uri = $uri->withQuery(http_build_query($queryParameters)); } return $uri; }
/** * Format message based on log container name. * * @param string $container * @param string $message * @param array $context * @return string */ public function formatMessage($container, $message, $context) { //We have to do interpolation first $message = \Spiral\interpolate($message, $context); \SqlFormatter::$pre_attributes = ''; if (strpos($container, 'Spiral\\Database\\Drivers') === 0 && isset($context['query'])) { //SQL queries from drivers return $this->highlightSQL($message); } return $message; }
/** * Generate set of altering operations should be applied to column to change it's type, size, * default value or null flag. * * @param ColumnSchema $initial * @return array */ public function alteringOperations(ColumnSchema $initial) { $operations = []; $currentDefinition = [$this->type, $this->size, $this->precision, $this->scale, $this->nullable]; $initialDefinition = [$initial->type, $initial->size, $initial->precision, $initial->scale, $initial->nullable]; if ($currentDefinition != $initialDefinition) { if ($this->abstractType() == 'enum') { //Getting longest value $enumSize = $this->size; foreach ($this->enumValues as $value) { $enumSize = max($enumSize, strlen($value)); } $type = "ALTER COLUMN {$this->getName(true)} varchar({$enumSize})"; $operations[] = $type . ' ' . ($this->nullable ? 'NULL' : 'NOT NULL'); } else { $type = "ALTER COLUMN {$this->getName(true)} {$this->type}"; if (!empty($this->size)) { $type .= "({$this->size})"; } elseif ($this->type == 'varchar' || $this->type == 'varbinary') { $type .= "(max)"; } elseif (!empty($this->precision)) { $type .= "({$this->precision}, {$this->scale})"; } $operations[] = $type . ' ' . ($this->nullable ? 'NULL' : 'NOT NULL'); } } //Constraint should be already removed it this moment (see doColumnChange in TableSchema) if ($this->hasDefaultValue()) { $operations[] = \Spiral\interpolate("ADD CONSTRAINT {constraint} DEFAULT {default} FOR {column}", ['constraint' => $this->defaultConstrain(true), 'column' => $this->getName(true), 'default' => $this->prepareDefault()]); } //Constraint should be already removed it this moment (see doColumnChange in TableSchema) if ($this->abstractType() == 'enum') { $operations[] = "ADD {$this->enumStatement()}"; } return $operations; }
/** * {@inheritdoc} */ public function report() { $this->debugger->logger()->error($this->getMessage()); if (!$this->config['reporting']['enabled']) { //No need to record anything return; } //Snapshot filename $filename = \Spiral\interpolate($this->config['reporting']['filename'], ['date' => date($this->config['reporting']['dateFormat'], time()), 'exception' => $this->getName()]); //Writing to hard drive $this->files->write($this->config['reporting']['directory'] . '/' . $filename, $this->render(), FilesInterface::RUNTIME, true); $snapshots = $this->files->getFiles($this->config['reporting']['directory']); if (count($snapshots) > $this->config['reporting']['maxSnapshots']) { $oldestSnapshot = ''; $oldestTimestamp = PHP_INT_MAX; foreach ($snapshots as $snapshot) { $snapshotTimestamp = $this->files->time($snapshot); if ($snapshotTimestamp < $oldestTimestamp) { $oldestTimestamp = $snapshotTimestamp; $oldestSnapshot = $snapshot; } } $this->files->delete($oldestSnapshot); } }
/** * Generate set of altering operations should be applied to column to change it's type, size, * default value or null flag. * * @param ColumnSchema $original * @return array */ public function alterOperations(ColumnSchema $original) { $operations = []; $typeDefinition = [$this->type, $this->size, $this->precision, $this->scale, $this->nullable]; $originalType = [$original->type, $original->size, $original->precision, $original->scale, $original->nullable]; if ($typeDefinition != $originalType) { if ($this->abstractType() == 'enum') { //Getting longest value $enumSize = $this->size; foreach ($this->enumValues as $value) { $enumSize = max($enumSize, strlen($value)); } $type = "ALTER COLUMN {$this->getName(true)} varchar({$enumSize})"; $operations[] = $type . ' ' . ($this->nullable ? 'NULL' : 'NOT NULL'); } else { $type = "ALTER COLUMN {$this->getName(true)} {$this->type}"; if (!empty($this->size)) { $type .= "({$this->size})"; } elseif ($this->type == 'varchar' || $this->type == 'varbinary') { $type .= "(max)"; } elseif (!empty($this->precision)) { $type .= "({$this->precision}, {$this->scale})"; } $operations[] = $type . ' ' . ($this->nullable ? 'NULL' : 'NOT NULL'); } } //Constraint should be already removed it this moment (see doColumnChange in TableSchema) if ($this->hasDefaultValue()) { if (!$this->defaultConstraint) { //Making new name $this->defaultConstraint = $this->table->getName() . '_' . $this->getName() . '_default_' . uniqid(); } $operations[] = \Spiral\interpolate("ADD CONSTRAINT {constraint} DEFAULT {default} FOR {column}", ['constraint' => $this->table->driver()->identifier($this->defaultConstraint), 'column' => $this->getName(true), 'default' => $this->prepareDefault()]); } //Constraint should be already removed it this moment (see doColumnChange in TableSchema) if ($this->abstractType() == 'enum') { $enumValues = []; foreach ($this->enumValues as $value) { $enumValues[] = $this->table->driver()->getPDO()->quote($value); } $operations[] = "ADD CONSTRAINT {$this->enumConstraint(true)} " . "CHECK ({$this->getName(true)} IN (" . join(', ', $enumValues) . "))"; } return $operations; }
/** * {@inheritdoc} */ public function log($level, $message, array $context = []) { if (!empty($this->debugger)) { //Global logging $this->debugger->logGlobal($this->name, $level, $message, $context); } if (empty($this->handlers)) { return $this; } $payload = [self::MESSAGE_CHANNEL => $this->name, self::MESSAGE_TIMESTAMP => microtime(true), self::MESSAGE_LEVEL => $level, self::MESSAGE_BODY => \Spiral\interpolate($message, $context), self::MESSAGE_CONTEXT => $context]; //We don't need this information for log handlers unset($payload[self::MESSAGE_CHANNEL], $payload[self::MESSAGE_TIMESTAMP]); if (isset($this->handlers[$level])) { call_user_func_array($this->handlers[$level], $payload); } elseif (isset($this->handlers[self::ALL])) { call_user_func_array($this->handlers[self::ALL], $payload); } return $this; }
/** * Register error message for specified field. Rule definition will be interpolated into * message. * * @param string $field * @param string $message * @param mixed $condition * @param array $arguments */ private function addMessage($field, $message, $condition, array $arguments = []) { if (is_array($condition)) { if (is_object($condition[0])) { $condition[0] = get_class($condition[0]); } $condition = join('::', $condition); } if ($this->options['names']) { $this->errors[$field] = \Spiral\interpolate($message, compact('field', 'condition') + $arguments); } else { $this->errors[$field] = \Spiral\interpolate($message, compact('condition') + $arguments); } }
/** * Request new migration filename based on user input and current timestamp. * * @param string $name * @return string */ private function createFilename($name) { $name = Inflector::tableize($name); $filename = \Spiral\interpolate(self::FILENAME_FORMAT, ['timestamp' => date(self::TIMESTAMP_FORMAT), 'chunk' => $this->chunkID++, 'name' => $name]); return $this->files->normalizePath($this->config->getDirectory() . '/' . $filename); }
/** * Add error to error log. * * @param Request $request * @param ClientException $exception */ private function logError(Request $request, ClientException $exception) { $remoteAddress = '-undefined-'; if (!empty($request->getServerParams()['REMOTE_ADDR'])) { $remoteAddress = $request->getServerParams()['REMOTE_ADDR']; } $this->logger()->error(\Spiral\interpolate(static::LOG_FORMAT, ['scheme' => $request->getUri()->getScheme(), 'host' => $request->getUri()->getHost(), 'path' => $request->getUri()->getPath(), 'code' => $exception->getCode(), 'message' => $exception->getMessage() ?: '-not specified-', 'remote' => $remoteAddress])); }
/** * Prepare highlighted lines for output. * * @param string $source * @param int $target * @param int $return * @return string */ private function lines($source, $target = null, $return = 10) { $lines = explode("\n", str_replace("\r\n", "\n", $source)); $result = ""; foreach ($lines as $number => $code) { $number++; if (empty($return) || $number >= $target - $return && $number <= $target + $return) { $template = $this->options['templates'][$number === $target ? 'highlighted' : 'line']; $result .= \Spiral\interpolate($template, ['number' => $number, 'source' => mb_convert_encoding($code, 'utf-8')]); } } return $result; }
/** * {@inheritdoc} */ protected function updateSchema() { if ($this->primaryKeys != $this->dbPrimaryKeys) { throw new SchemaException("Primary keys can not be changed for already exists table ({$this->getName()})."); } $this->driver->beginTransaction(); try { $rebuildRequired = false; if ($this->alteredColumns() || $this->alteredReferences()) { $rebuildRequired = true; } if (!$rebuildRequired) { foreach ($this->alteredIndexes() as $name => $schema) { $dbIndex = isset($this->dbIndexes[$name]) ? $this->dbIndexes[$name] : null; if (!$schema) { $this->logger()->info("Dropping index [{statement}] from table {table}.", ['statement' => $dbIndex->sqlStatement(true), 'table' => $this->getName(true)]); $this->doIndexDrop($dbIndex); continue; } if (!$dbIndex) { $this->logger()->info("Adding index [{statement}] into table {table}.", ['statement' => $schema->sqlStatement(false), 'table' => $this->getName(true)]); $this->doIndexAdd($schema); continue; } //Altering $this->logger()->info("Altering index [{statement}] to [{new}] in table {table}.", ['statement' => $dbIndex->sqlStatement(false), 'new' => $schema->sqlStatement(false), 'table' => $this->getName(true)]); $this->doIndexChange($schema, $dbIndex); } } else { $this->logger()->info("Rebuilding table {table} to apply required modifications.", ['table' => $this->getName(true)]); //To be renamed later $tableName = $this->name; $this->name = 'spiral_temp_' . $this->name . '_' . uniqid(); //SQLite index names are global $indexes = $this->indexes; $this->indexes = []; //Creating temporary table $this->createSchema(); //Mapping columns $mapping = []; foreach ($this->columns as $name => $schema) { if (isset($this->dbColumns[$name])) { $mapping[$schema->getName(true)] = $this->dbColumns[$name]->getName(true); } } $this->logger()->info("Migrating table data from {source} to {table} with columns mappings ({columns}) => ({target}).", ['source' => $this->driver->identifier($tableName), 'table' => $this->getName(true), 'columns' => join(', ', $mapping), 'target' => join(', ', array_keys($mapping))]); //http://stackoverflow.com/questions/4007014/alter-column-in-sqlite $query = \Spiral\interpolate("INSERT INTO {table} ({target}) SELECT {columns} FROM {source}", ['source' => $this->driver->identifier($tableName), 'table' => $this->getName(true), 'columns' => join(', ', $mapping), 'target' => join(', ', array_keys($mapping))]); $this->driver->statement($query); //Dropping original table $this->driver->statement('DROP TABLE ' . $this->driver->identifier($tableName)); //Renaming (without prefix) $this->rename(substr($tableName, strlen($this->tablePrefix))); //Restoring indexes, we can create them now $this->indexes = $indexes; foreach ($this->indexes as $index) { $this->doIndexAdd($index); } } } catch (\Exception $exception) { $this->driver->rollbackTransaction(); throw $exception; } $this->driver->commitTransaction(); }
/** * Drop table schema in database. This operation must be applied immediately. */ public function drop() { if (!$this->exists()) { $this->columns = $this->dbColumns = $this->primaryKeys = $this->dbPrimaryKeys = []; $this->indexes = $this->dbIndexes = $this->references = $this->dbReferences = []; return; } //Dropping syntax is the same everywhere, for now... $this->driver->statement(\Spiral\interpolate("DROP TABLE {table}", ['table' => $this->getName(true)])); $this->exists = false; $this->columns = $this->dbColumns = $this->primaryKeys = $this->dbPrimaryKeys = []; $this->indexes = $this->dbIndexes = $this->references = $this->dbReferences = []; }
/** * @param \Throwable $exception * @param int $time * @return string */ public function snapshotFilename($exception, $time) { $name = (new \ReflectionObject($exception))->getShortName(); $filename = \Spiral\interpolate($this->config['reporting']['filename'], ['date' => date($this->config['reporting']['dateFormat'], $time), 'name' => $name]); return $this->reportingDirectory() . $filename; }
/** * {@inheritdoc} * * You can use custom pluralizer to create more complex word forms, for example day postfix and * etc. The only problem will be that system will not be able to index such things * automatically. * * @param PluralizerInterface $pluralizer Custom pluralizer to be used. */ public function pluralize($phrase, $number, $format = true, PluralizerInterface $pluralizer = null) { $this->loadBundle($bundle = $this->config['plurals']); if (empty($pluralizer)) { //Active pluralizer $pluralizer = $this->pluralizer(); } if (!isset($this->bundles[$bundle][$phrase = $this->normalize($phrase)])) { $this->bundles[$bundle][$phrase] = array_pad([], $pluralizer->countForms(), func_get_arg(0)); $this->saveBundle($bundle); } if (is_null($number)) { return $this->bundles[$bundle][$phrase]; } return \Spiral\interpolate($pluralizer->getForm($number, $this->bundles[$bundle][$phrase]), ['n' => $format ? number_format($number) : $number]); }
/** * {@inheritdoc} */ public function alterIndex(AbstractTable $table, AbstractIndex $initial, AbstractIndex $index) { $query = \Spiral\interpolate("ALTER TABLE {table} DROP INDEX {index}, ADD {statement}", ['table' => $table->getName(true), 'index' => $initial->getName(true), 'statement' => $index->sqlStatement(false)]); $this->run($query); return $this; }
/** * {@inheritdoc} */ protected function doColumnChange(AbstractColumn $column, AbstractColumn $dbColumn) { /** * @var ColumnSchema $column * @var ColumnSchema $dbColumn */ //Renaming is separate operation if ($column->getName() != $dbColumn->getName()) { $this->driver->statement("sp_rename ?, ?, 'COLUMN'", [$this->getName() . '.' . $dbColumn->getName(), $column->getName()]); $column->setName($dbColumn->getName()); } //In SQLServer we have to drop ALL related indexes and foreign keys while //applying type change... yeah... $indexesBackup = []; $foreignBackup = []; foreach ($this->indexes as $index) { if (in_array($column->getName(), $index->getColumns())) { $indexesBackup[] = $index; $this->doIndexDrop($index); } } foreach ($this->references as $foreign) { if ($foreign->getColumn() == $column->getName()) { $foreignBackup[] = $foreign; $this->doForeignDrop($foreign); } } //Column will recreate needed constraints foreach ($column->getConstraints() as $constraint) { $this->doConstraintDrop($constraint); } foreach ($column->alterOperations($dbColumn) as $operation) { $query = \Spiral\interpolate('ALTER TABLE {table} {operation}', ['table' => $this->getName(true), 'operation' => $operation]); $this->driver->statement($query); } //Recreating indexes foreach ($indexesBackup as $index) { $this->doIndexAdd($index); } foreach ($foreignBackup as $foreign) { $this->doForeignAdd($foreign); } }