From 5dd2a0af301c07fa81f75280a0f4a48148ca424d Mon Sep 17 00:00:00 2001 From: Charles Severance Date: Mon, 22 Mar 2021 17:58:29 -0400 Subject: [PATCH] Begin experimental support for PostgreSQL (#114) --- admin/blob/database.php | 4 +- admin/mail/database.php | 2 +- admin/migrate-run.php | 12 +- composer.json | 20 ++- composer.lock | 10 +- vendor/composer/InstalledVersions.php | 6 +- vendor/composer/installed.json | 8 +- vendor/composer/installed.php | 6 +- vendor/tsugi/lib/src/Core/LTIX.php | 2 + vendor/tsugi/lib/src/Core/SQLDialect.php | 151 +++++++++++++++++++++++ vendor/tsugi/lib/src/Util/PDOX.php | 72 ++++++++--- vendor/tsugi/lib/src/Util/PS.php | 37 ++++++ 12 files changed, 288 insertions(+), 42 deletions(-) create mode 100644 vendor/tsugi/lib/src/Core/SQLDialect.php diff --git a/admin/blob/database.php b/admin/blob/database.php index 03d99ec0a8..b9d1f682e9 100644 --- a/admin/blob/database.php +++ b/admin/blob/database.php @@ -6,7 +6,7 @@ $DATABASE_INSTALL = array( array( "{$CFG->dbprefix}blob_file", "create table {$CFG->dbprefix}blob_file ( - file_id INTEGER NOT NULL KEY AUTO_INCREMENT, + file_id INTEGER NOT NULL AUTO_INCREMENT, file_sha256 CHAR(64) NOT NULL, context_id INTEGER NULL, @@ -28,6 +28,8 @@ INDEX `{$CFG->dbprefix}blob_indx_2` ( path (128) ), INDEX `{$CFG->dbprefix}blob_indx_4` ( context_id ), + CONSTRAINT `{$CFG->dbprefix}lti_blob_file_pk` PRIMARY KEY (file_id), + CONSTRAINT `{$CFG->dbprefix}blob_ibfk_1` FOREIGN KEY (`context_id`) REFERENCES `{$CFG->dbprefix}lti_context` (`context_id`) diff --git a/admin/mail/database.php b/admin/mail/database.php index 797a8a1584..54a37d2c94 100644 --- a/admin/mail/database.php +++ b/admin/mail/database.php @@ -33,7 +33,7 @@ ) ENGINE = InnoDB DEFAULT CHARSET=utf8"), array( "{$CFG->dbprefix}mail_sent", -"create table {$CFG->dbprefix}mail_sent( +"create table {$CFG->dbprefix}mail_sent ( sent_id INTEGER NOT NULL AUTO_INCREMENT, context_id INTEGER NOT NULL, diff --git a/admin/migrate-run.php b/admin/migrate-run.php index 949b183f20..41c4daad80 100644 --- a/admin/migrate-run.php +++ b/admin/migrate-run.php @@ -17,17 +17,17 @@ echo("-- Creating table ".$entry[0]."
\n"); error_log("-- Creating table ".$entry[0]); $q = $PDOX->queryReturnError($entry[1]); - if ( ! $q->success ) die("Unable to create ".$entry[1]." ".$q->errorImplode."
".$entry[1] ); + if ( ! $q->success ) die("Unable to create ".$entry[0]." ".$q->errorImplode."
".$q->sqlQuery ); $OUTPUT->togglePre("-- Created table ".$entry[0], $entry[1]); $sql = "INSERT INTO {$plugins} ( plugin_path, version, created_at, updated_at ) VALUES ( :plugin_path, :version, NOW(), NOW() ) - ON DUPLICATE KEY + ON DUPLICATE KEY /* plugin_path */ UPDATE version = :version, updated_at = NOW()"; $values = array( ":plugin_path" => $path, ":version" => $CFG->dbversion); $q = $PDOX->queryReturnError($sql, $values); - if ( ! $q->success ) die("Unable to set version for ".$path." ".$q->errorimplode."
".$entry[1] ); + if ( ! $q->success ) die("Unable to set version for ".$path." ".$q->errorImplode."
".$q->sqlQuery ); // Do the POST-Create if ( isset($DATABASE_POST_CREATE) && $DATABASE_POST_CREATE !== false ) { $DATABASE_POST_CREATE($entry[0]); @@ -48,7 +48,7 @@ $values = array( ":plugin_path" => $path, ":version" => $CFG->dbversion); $q = $PDOX->queryReturnError($sql, $values); - if ( ! $q->success ) die("Unable to set version for ".$path." ".$q->errorimplode."
".$entry[1] ); + if ( ! $q->success ) die("Unable to set version for ".$path." ".$q->errorImplode."
".$q->sqlQuery ); $delta = time() - $ticks; if ( $delta > 1 ) echo("--- Ellapsed time=".$delta." seconds
\n"); $ticks = time(); @@ -88,11 +88,11 @@ $sql = "INSERT INTO {$plugins} ( plugin_path, version, created_at, updated_at ) VALUES ( :plugin_path, :version, NOW(), NOW() ) - ON DUPLICATE KEY + ON DUPLICATE KEY /* plugin_path */ UPDATE version = :version, updated_at = NOW()"; $values = array( ":version" => $newversion, ":plugin_path" => $path); $q = $PDOX->queryReturnError($sql, $values); - if ( ! $q->success ) die("Unable to update version for ".$path." ".$q->errorimplode."
".$entry[1] ); + if ( ! $q->success ) die("Unable to update version for ".$path." ".$q->errorImplode."
".$q->sqlQuery ); } // Make sure these do not run twice diff --git a/composer.json b/composer.json index f2c00a52c2..e084f2e32a 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,25 @@ "symfony/polyfill-php73": "v1.22.0", "symfony/polyfill-php80": "v1.22.0", - "tsugi/lib": "dev-master#20f59d1ba8d8b558830df55497ba137a887bc504", + "guzzlehttp/promises": "1.4.0", + "guzzlehttp/psr7": "1.7.0", + "nesbot/carbon": "2.45.1", + "psr/container": "1.0.0", + "react/dns": "v1.4.0", + "symfony/console": "v5.2.3", + "symfony/error-handler": "v5.2.3", + "symfony/event-dispatcher": "v5.2.3", + "symfony/finder": "v5.2.3", + "symfony/http-foundation": "v5.2.3", + "symfony/http-kernel": "v5.2.3", + "symfony/mime": "v5.2.3", + "symfony/process": "v5.2.3", + "symfony/routing": "v5.2.3", + "symfony/string": "v5.2.3", + "symfony/translation": "v5.2.3", + "symfony/var-dumper": "v5.2.3", + + "tsugi/lib": "dev-master#d7e042ccb6aac340f41726e502fe271dbaa30d8e", "koseu/lib": "dev-master#5c5bcb32469977bea262b1900461c3f205adb899" }, "config": { diff --git a/composer.lock b/composer.lock index 9a2ccc0cd6..e6a9a6c99f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fb020ee147470f45a69b9452090804de", + "content-hash": "f4efc50074337430f3cc63382de21df8", "packages": [ { "name": "aws/aws-sdk-php", @@ -6123,12 +6123,12 @@ "source": { "type": "git", "url": "https://github.com/tsugiproject/tsugi-php.git", - "reference": "20f59d1ba8d8b558830df55497ba137a887bc504" + "reference": "d7e042ccb6aac340f41726e502fe271dbaa30d8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tsugiproject/tsugi-php/zipball/20f59d1ba8d8b558830df55497ba137a887bc504", - "reference": "20f59d1ba8d8b558830df55497ba137a887bc504", + "url": "https://api.github.com/repos/tsugiproject/tsugi-php/zipball/d7e042ccb6aac340f41726e502fe271dbaa30d8e", + "reference": "d7e042ccb6aac340f41726e502fe271dbaa30d8e", "shasum": "" }, "require": { @@ -6171,7 +6171,7 @@ "issues": "https://github.com/tsugiproject/tsugi-php/issues", "source": "https://github.com/tsugiproject/tsugi-php/tree/master" }, - "time": "2021-02-28T03:45:33+00:00" + "time": "2021-03-21T18:58:10+00:00" }, { "name": "vlucas/phpdotenv", diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php index 68f9bef499..29153136aa 100644 --- a/vendor/composer/InstalledVersions.php +++ b/vendor/composer/InstalledVersions.php @@ -29,7 +29,7 @@ class InstalledVersions 'aliases' => array ( ), - 'reference' => 'f1a39eac90658eef699859eb548b80a7230ac210', + 'reference' => '401febcb05e0dd53adc06595f5e00ab5b0037515', 'name' => '__root__', ), 'versions' => @@ -41,7 +41,7 @@ class InstalledVersions 'aliases' => array ( ), - 'reference' => 'f1a39eac90658eef699859eb548b80a7230ac210', + 'reference' => '401febcb05e0dd53adc06595f5e00ab5b0037515', ), 'aws/aws-sdk-php' => array ( @@ -930,7 +930,7 @@ class InstalledVersions array ( 0 => '9999999-dev', ), - 'reference' => '20f59d1ba8d8b558830df55497ba137a887bc504', + 'reference' => 'd7e042ccb6aac340f41726e502fe271dbaa30d8e', ), 'vlucas/phpdotenv' => array ( diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index f03ea23742..30e709671e 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -5941,12 +5941,12 @@ "source": { "type": "git", "url": "https://github.com/tsugiproject/tsugi-php.git", - "reference": "20f59d1ba8d8b558830df55497ba137a887bc504" + "reference": "d7e042ccb6aac340f41726e502fe271dbaa30d8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tsugiproject/tsugi-php/zipball/20f59d1ba8d8b558830df55497ba137a887bc504", - "reference": "20f59d1ba8d8b558830df55497ba137a887bc504", + "url": "https://api.github.com/repos/tsugiproject/tsugi-php/zipball/d7e042ccb6aac340f41726e502fe271dbaa30d8e", + "reference": "d7e042ccb6aac340f41726e502fe271dbaa30d8e", "shasum": "" }, "require": { @@ -5959,7 +5959,7 @@ "phpunit/php-timer": ">=1.0.6", "phpunit/phpunit": "8.*" }, - "time": "2021-02-28T03:45:33+00:00", + "time": "2021-03-21T18:58:10+00:00", "default-branch": true, "type": "library", "installation-source": "dist", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index e5325712a5..cfd84d8612 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -6,7 +6,7 @@ 'aliases' => array ( ), - 'reference' => 'f1a39eac90658eef699859eb548b80a7230ac210', + 'reference' => '401febcb05e0dd53adc06595f5e00ab5b0037515', 'name' => '__root__', ), 'versions' => @@ -18,7 +18,7 @@ 'aliases' => array ( ), - 'reference' => 'f1a39eac90658eef699859eb548b80a7230ac210', + 'reference' => '401febcb05e0dd53adc06595f5e00ab5b0037515', ), 'aws/aws-sdk-php' => array ( @@ -907,7 +907,7 @@ array ( 0 => '9999999-dev', ), - 'reference' => '20f59d1ba8d8b558830df55497ba137a887bc504', + 'reference' => 'd7e042ccb6aac340f41726e502fe271dbaa30d8e', ), 'vlucas/phpdotenv' => array ( diff --git a/vendor/tsugi/lib/src/Core/LTIX.php b/vendor/tsugi/lib/src/Core/LTIX.php index 29519546c5..6a7eba94ac 100644 --- a/vendor/tsugi/lib/src/Core/LTIX.php +++ b/vendor/tsugi/lib/src/Core/LTIX.php @@ -15,6 +15,7 @@ use \Tsugi\UI\Output; use \Tsugi\Core\I18N; use \Tsugi\Core\Settings; +use \Tsugi\Core\SQLDialect; use \Tsugi\OAuth\OAuthUtil; use \Tsugi\Crypt\SecureCookie; use \Tsugi\Crypt\AesCtr; @@ -74,6 +75,7 @@ public static function getConnection() { } } if ( isset($CFG->slow_query) ) $PDOX->slow_query = $CFG->slow_query; + $PDOX->sqlPatch = function($PDOX, $sql) { return \Tsugi\Core\SQLDialect::sqlPatch($PDOX, $sql); } ; return $PDOX; } diff --git a/vendor/tsugi/lib/src/Core/SQLDialect.php b/vendor/tsugi/lib/src/Core/SQLDialect.php new file mode 100644 index 0000000000..b6a891d732 --- /dev/null +++ b/vendor/tsugi/lib/src/Core/SQLDialect.php @@ -0,0 +1,151 @@ +isMySQL() ) { + return $sql; + } + if ( ! $PDOX->isPgSQL() ) { + die('Only MySQL and PostgreSQL are supported'); + } + + // echo("Dialect\n".$sql."\n"); + $pieces = (new PS($sql))->split(); + if ( count($pieces) < 1 ) return $sql; + if ( strcasecmp($pieces[0], "create") == 0 ) { + return self::sqlCreate2Postgres($PDOX, $sql); + } else if ( strcasecmp($pieces[0], "insert") == 0 ) { + return self::sqlInsert2Postgres($PDOX, $sql); + } else if ( strcasecmp($pieces[0], "alter") == 0 ) { + return self::sqlAlter2Postgres($PDOX, $sql); + } + return $sql; + } + + public static function sqlCreate2Postgres($PDOX, $sql) { + + $nsql = self::patchPostgresQuotes($PDOX, $sql); + $nsql = self::patchDataTypes($PDOX, $nsql); + + // ) ENGINE = InnoDB DEFAULT CHARSET=utf8;"; + // ) COLLATE utf8_bin, ENGINE = InnoDB; + $nsql = preg_replace('/\).*ENGINE\s*=\s*InnoDB.*$/i', ');', $nsql); + $nsql = preg_replace('/\s+USING\s+HASH\s+/i', ' ', $nsql); + + return $nsql; + } + + // ON DUPLICATE KEY /* plugin_path */ UPDATE + // ON CONFLICT (plugin_path) DO UPDATE SET + public static function sqlInsert2Postgres($PDOX, $sql) { + $nsql = self::patchPostgresQuotes($PDOX, $sql); + + $matches = array(); + preg_match('/ON\s+DUPLICATE\s+KEY\s\/\*\s+[^ *]*\s+\*\/\s+UPDATE\s+/i', $nsql, $matches, PREG_OFFSET_CAPTURE); + if ( count($matches) < 1 ) return $nsql; + $str = $matches[0][0]; + $pos = $matches[0][1]; + $len = strlen($str); + $str = preg_replace('/\sDUPLICATE\s+KEY\s/i', ' CONFLICT ', $str); + $str = preg_replace('/\/\*/i', '(', $str); + $str = preg_replace('/\*\//i', ')', $str); + $str = preg_replace('/\s+UPDATE\s+/i', " DO UPDATE SET\n", $str); + + $newsql = substr($nsql,0,$pos) . $str . substr($nsql, $pos+$len); + return $newsql; + } + + public static function sqlAlter2Postgres($PDOX, $sql) { + + $nsql = self::patchPostgresQuotes($PDOX, $sql); + $nsql = self::patchDataTypes($PDOX, $nsql); + + // ALTER TABLE lms_tools_status MODIFY commit_log MEDIUMTEXT NULL + // ALTER TABLE lms_tools_status ALTER COLUMN column_name TYPE new_data_type; + // ALTER TABLE lms_tools_status ALTER COLUMN column_name DROP NOT NULL; + $matches = array(); + preg_match('/\s+MODIFY\s+[^\s]*\s+/i', $nsql, $matches, PREG_OFFSET_CAPTURE); + if ( count($matches) < 1 ) return $nsql; + $str = $matches[0][0]; + $pos = $matches[0][1]; + $len = strlen($str); + $str = preg_replace('/\sMODIFY\s/i', ' ALTER COLUMN ', $str); + + $tail = substr($nsql, $pos+$len); + $pieces = (new PS($tail))->split(); + if ( count($pieces) < 1 ) { + $newsql = substr($nsql,0,$pos) . $str . ' TYPE ' .substr($nsql, $pos+$len); + return $newsql; + } + + // Normal flow + $retval = array(); + $newsql = substr($nsql,0,$pos) . $str . ' TYPE ' . array_shift($pieces); + $retval[] = $newsql; + + $alter = substr($nsql,0,$pos) . $str . ' '; + if ( count($pieces) == 1 && strcasecmp($pieces[0], "null") == 0 ) { + $newsql = $alter . "DROP NOT NULL;"; + $retval[] = $newsql; + } else if (count($pieces) == 2 && strcasecmp($pieces[0], "NOT") == 0 && + strcasecmp($pieces[0], "NULL") == 0 ) { + $newsql = $alter . "SET NOT NULL;"; + $retval[] = $newsql; + } + if ( count($retval) == 1 ) { + return $retval[0]; + } + return $retval; + } + + + public static function patchDataTypes($PDOX, $sql) { + // https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_serial + // https://www.postgresqltutorial.com/postgresql-identity-column/ + // https://www.depesz.com/2017/04/10/waiting-for-postgresql-10-identity-columns/ + // plugin_id INTEGER NOT NULL AUTO_INCREMENT, + // plugin_id INTEGER NOT NULL primary key generated always as identity + $nsql = preg_replace('/AUTO_INCREMENT/i', "GENERATED BY DEFAULT AS IDENTITY", $sql); + + // created_at DATETIME NOT NULL, + // created_at TIMESTAMP(0) NOT NULL, + $nsql = preg_replace('/\sDATETIME([\s,])/i', ' TIMESTAMP(0)$1', $nsql); + + // deleted TINYINT(1) NOT NULL DEFAULT 0, + // deleted SMALLINT NOT NULL DEFAULT 0, + $nsql = preg_replace('/\sTINYINT\(1\)([\s,])/i', ' SMALLINT$1', $nsql); + + $nsql = preg_replace('/\sMEDIUMTEXT([\s,])/i', ' TEXT$1', $nsql); + + $nsql = preg_replace('/\sMEDIUMINT([\s,])/i', ' INTEGER$1', $nsql); + $nsql = preg_replace('/\sTINYINT([\s,])/i', ' INTEGER$1', $nsql); + + $nsql = preg_replace('/\sUNSIGNED([\s,])/i', '$1', $nsql); + + $nsql = preg_replace('/\sVARBINARY\([0-9]+\)([\s,])/i', ' BYTEA$1', $nsql); + + $nsql = preg_replace('/\sBINARY\([0-9]+\)([\s,])/i', ' BYTEA$1', $nsql); + + $nsql = preg_replace('/\sDOUBLE([\s,])/i', ' DOUBLE PRECISION$1', $nsql); + + $nsql = preg_replace('/\sBLOB([\s,])/i', ' BYTEA$1', $nsql); + + return $nsql; + } + + public static function patchPostgresQuotes($PDOX, $sql) { + $nsql = str_replace('`', '"', $sql); + return $nsql; + } + +} diff --git a/vendor/tsugi/lib/src/Util/PDOX.php b/vendor/tsugi/lib/src/Util/PDOX.php index b767a7e959..76cd329cff 100644 --- a/vendor/tsugi/lib/src/Util/PDOX.php +++ b/vendor/tsugi/lib/src/Util/PDOX.php @@ -88,12 +88,30 @@ function queryReturnError($sql, $arr=FALSE, $error_log=TRUE) { if ( $arr !== FALSE && ! is_array($arr) ) $arr = Array($arr); $start = microtime(true); // debug_log($sql, $arr); - try { - $q = $this->prepare($sql); - if ( $arr === FALSE ) { - $success = $q->execute(); + + // Optionally patch the SQL to support different variants + $todo = array(); + $todo[] = $sql; + if ( isset($this->sqlPatch) && is_callable($this->sqlPatch) ) { + $func = $this->sqlPatch; + $check = $func($this, $sql); + if ( is_array($check) ) { + $todo = $check; } else { - $success = $q->execute($arr); + $todo = array(); + $todo[] = $check; + } + } + + try { + foreach($todo as $query) { + $q = $this->prepare($query); + if ( $arr === FALSE ) { + $success = $q->execute(); + } else { + $success = $q->execute($arr); + } + if ( ! $success ) break; } } catch(\Exception $e) { $success = FALSE; @@ -121,6 +139,8 @@ function queryReturnError($sql, $arr=FALSE, $error_log=TRUE) { if ( !isset($q->errorCode) ) $q->errorCode = '42000'; if ( !isset($q->errorInfo) ) $q->errorInfo = Array('42000', '42000', $message); if ( !isset($q->errorImplode) ) $q->errorImplode = implode(':',$q->errorInfo); + if ( !isset($q->sqlQuery) ) $q->sqlQuery = implode('; ', $todo); + if ( !isset($q->sqlOriginalQuery) ) $q->sqlOriginalQuery = $sql; // Restore ERRMODE if we changed it if ( $errormode != \PDO::ERRMODE_EXCEPTION) { $this->setAttribute(\PDO::ATTR_ERRMODE, $errormode); @@ -201,30 +221,28 @@ function allRowsDie($sql, $arr=FALSE, $error_log=TRUE) { * Retrieve the metadata for a table. */ function metadata($tablename) { - $sql = "SHOW COLUMNS FROM ".$tablename; + if ( $this->isMySQL() ) { + $sql = "SHOW COLUMNS FROM ".$tablename; + } else { + $sql = 'SELECT column_name AS "Field", data_type AS "Type", is_nullable AS "Null" + FROM information_schema.columns WHERE table_name = \''.$tablename.'\';'; + } $stmt = self::queryReturnError($sql); if ( $stmt->success ) { $retval= $stmt->fetchAll(); + if ( count($retval) == 0 ) $retval = false; } else { $retval = false; } $stmt->closeCursor(); - return $retval; + return $retval; } /** * Retrieve the metadata for a table. */ function describe($tablename) { - $sql = "DESCRIBE ".$tablename; - $stmt = self::queryReturnError($sql); - if ( $stmt->success ) { - $retval= $stmt->fetchAll(); - } else { - $retval = false; - } - $stmt->closeCursor(); - return $retval; + return $this->metadata($tablename); } /** @@ -279,8 +297,9 @@ function columnIsNull($fieldname, $source) function columnExists($fieldname, $source) { if ( is_string($source) ) { // Demand table exists - $source = self::describe($source); - if ( ! $source ) throw new \Exception("Could not find $source"); + $check = self::describe($source); + if ( ! $check ) throw new \Exception("Could not find $source"); + $source = $check; } $column = self::describeColumn($fieldname, $source); return is_array($column); @@ -359,4 +378,21 @@ function versionAtLeast($min) return (version_compare($version, $min) >= 0); } + /** + * Return true if the current connection is MySQL + */ + function isMySQL() + { + $name = $this->getAttribute(\PDO::ATTR_DRIVER_NAME); + return $name == 'mysql'; + } + + /** + * Return true if the current connection is PgSQL + */ + function isPgSQL() + { + $name = $this->getAttribute(\PDO::ATTR_DRIVER_NAME); + return $name == 'pgsql'; + } } diff --git a/vendor/tsugi/lib/src/Util/PS.php b/vendor/tsugi/lib/src/Util/PS.php index 5cba6503a3..05643a5b43 100644 --- a/vendor/tsugi/lib/src/Util/PS.php +++ b/vendor/tsugi/lib/src/Util/PS.php @@ -88,4 +88,41 @@ public function rfind($sub, $start=0, $end=-1) { } return strrpos($tmp, $sub); } + + /** + * emulate python strip() + * + * string.strip(characters) + * + * characters Optional. A set of characters to remove as leading/trailing characters + * + * txt = ",,,,,rrttgg.....banana....rrr" + * x = txt.strip(",.grt") + */ + public function strip($characters=false) { + if ( ! is_string($characters) ) return trim($this->internal); + return trim($this->internal, $characters); + } + + /** + * emulate the Python split() + * + * string.split(separator, maxsplit) + * + * separator Optional. Specifies the separator to use when splitting the string. By default any whitespace is a separator + * maxsplit Optional. Specifies how many splits to do. Default value is -1, which is "all occurrences" + * + * Note: When maxsplit is specified, the list will contain the specified number of elements plus one. + */ + public function split($separator=false,$maxsplit=false) { + if ( $separator == false ) { + $retval = preg_split('/\s+/', $this->strip()); + } else { + $retval = explode($separator, $this->internal); + } + if ( $maxsplit !== false && count($retval) > $maxsplit + 1 ) { + return array_slice($retval, 0, $maxsplit+1); + } + return $retval; + } }