* * -------------------------------------------- * * This program is free software; you can redistribute it and/or modify it * * under the terms of the GNU General Public License as published by the * * Free Software Foundation; either version 2 of the License, or (at your * * option) any later version. * \**************************************************************************/ /* $Id$ */ class db_tools { var $public_functions = array ( 'edit' => True, 'needs_save' => True, //'admin' => True, //'preferences' => True ); var $debug = 0; var $editor; // editor eTemplate var $data; // Table definitions var $app; // used app var $table; // used table var $types = array( 'varchar' => 'varchar', 'int' => 'int', 'auto' => 'auto', 'blob' => 'blob', 'char' => 'char', 'date' => 'date', 'decimal' => 'decimal', 'float' => 'float', 'longtext' => 'longtext', 'text' => 'text', 'timestamp' => 'timestamp', // 'abstime' => 'abstime (mysql:timestamp)', ); var $setup_header = 'editor = CreateObject('etemplate.etemplate','etemplate.db-tools.edit'); $this->data = array(); if (!is_array($GLOBALS['phpgw_info']['apps']) || !count($GLOBALS['phpgw_info']['apps'])) { ExecMethod('phpgwapi.applications.read_installed_apps'); } $GLOBALS['phpgw_info']['flags']['app_header'] = $GLOBALS['phpgw_info']['apps']['etemplate']['title'].' - '.lang('DB-Tools'); } /*! @function edit @syntax edit( $content='',$msg='' ) @author ralfbecker @abstract this is the table editor (and the callback/submit-method too) */ function edit($content='',$msg = '') { if (isset($_GET['app'])) { $this->app = $_GET['app']; } if (is_array($content)) { if ($this->debug) { echo "content ="; _debug_array($content); } $this->app = $content['app']; // this is what the user selected $this->table = $content['table_name']; $posted_app = $content['posted_app']; // this is the old selection $posted_table = $content['posted_table']; } if ($posted_app && $posted_table && // user changed app or table ($posted_app != $this->app || $posted_table != $this->table)) { if ($this->needs_save('',$posted_app,$posted_table,$this->content2table($content))) { return; } $this->renames = array(); } if (!$this->app) { $this->table = ''; $table_names = array('' => lang('none')); } else { $this->read($this->app,$this->data); foreach($this->data as $name => $table) { $table_names[$name] = $name; } } if (!$this->table || $this->app != $posted_app) { reset($this->data); list($this->table) = each($this->data); // use first table } elseif ($this->app == $posted_app && $posted_table) { $this->data[$posted_table] = $this->content2table($content); } if ($content['write_tables']) { if ($this->needs_save('',$this->app,$this->table,$this->data[$posted_table])) { return; } $msg .= lang('Table unchanged, no write necessary !!!'); } elseif ($content['delete']) { list($col) = each($content['delete']); @reset($this->data[$posted_table]['fd']); while ($col-- > 0 && list($key,$data) = @each($this->data[$posted_table]['fd'])) ; unset($this->data[$posted_table]['fd'][$key]); $this->changes[$posted_table][$key] = '**deleted**'; } elseif ($content['add_column']) { $this->data[$posted_table]['fd'][''] = array(); } elseif ($content['add_table'] || $content['import']) { if (!$this->app) { $msg .= lang('Select an app first !!!'); } elseif (!$content['new_table_name']) { $msg .= lang('Please enter table-name first !!!'); } elseif ($content['add_table']) { $this->table = $content['new_table_name']; $this->data[$this->table] = array('fd' => array(),'pk' =>array(),'ix' => array(),'uc' => array(),'fk' => array()); $msg .= lang('New table created'); } else // import { $oProc = CreateObject('phpgwapi.schema_proc',$GLOBALS['phpgw_info']['server']['db_type']); $oProc->m_odb = $GLOBALS['phpgw']->db; $oProc->m_oTranslator->_GetColumns($oProc,$content['new_table_name'],$nul); while (list($key,$tbldata) = each ($oProc->m_oTranslator->sCol)) { $cols .= $tbldata; } eval('$cols = array('. $cols . ');'); $this->data[$this->table = $content['new_table_name']] = array( 'fd' => $cols, 'pk' => $oProc->m_oTranslator->pk, 'fk' => $oProc->m_oTranslator->fk, 'ix' => $oProc->m_oTranslator->ix, 'uc' => $oProc->m_oTranslator->uc ); } } elseif ($content['editor']) { ExecMethod('etemplate.editor.edit'); return; } $add_index = isset($content['add_index']); // from here on, filling new content for eTemplate $content = array( 'msg' => $msg, 'table_name' => $this->table, 'app' => $this->app, ); if (!isset($table_names[$this->table])) // table is not jet written { $table_names[$this->table] = $this->table; } $sel_options = array( 'table_name' => $table_names, 'type' => $this->types ); if ($this->table != '' && isset($this->data[$this->table])) { $content += $this->table2content($this->data[$this->table],$sel_options['Index'],$add_index); } $no_button = array( ); if (!$this->app || !$this->table) { $no_button += array('write_tables' => True); } if ($this->debug) { echo 'editor.edit: content ='; _debug_array($content); } $this->editor->exec('etemplate.db_tools.edit',$content,$sel_options,$no_button, array('posted_table' => $this->table,'posted_app' => $this->app,'changes' => $this->changes)); } /*! @function needs_save @syntax needs_save( $cont='',$posted_app='',$posted_table='',$edited_table='' ) @author ralfbecker @abstract checks if table was changed and if so offers user to save changes @param $cont the content of the form (if called by process_exec) @param $posted_app the app the table is from @param $posted_table the table-name @param $edited_table the edited table-definitions @result only if no changes */ function needs_save($cont='',$posted_app='',$posted_table='',$edited_table='') { if (!$posted_app && is_array($cont)) { if (isset($cont['yes'])) { $this->app = $cont['app']; $this->table = $cont['table']; $this->read($this->app,$this->data); $this->data[$this->table] = $cont['edited_table']; $this->changes = $cont['changes']; if ($cont['new_version']) { $this->update($this->app,$this->data,$cont['new_version']); } else { foreach($this->data as $tname => $tinfo) { $tables .= ($tables ? ',' : '') . "'$tname'"; } $this->setup_version($this->app,'',$tables); } $msg .= $this->write($this->app,$this->data) ? lang('File writen') : lang('Error: writing file (no write-permission for the webserver) !!!'); } $this->changes = array(); // return to edit with everything set, so the user gets the table he asked for $this->edit(array( 'app' => $cont['new_app'], 'table_name' => $cont['app']==$cont['new_app'] ? $cont['new_table'] : '', 'posted_app' => $cont['new_app'] ),$msg); return True; } $new_app = $this->app; // these are the ones, the users whiches to change too $new_table = $this->table; $this->app = $posted_app; $this->data = array(); $this->read($posted_app,$this->data); if (isset($this->data[$posted_table]) && $this->tables_identical($this->data[$posted_table],$edited_table)) { if ($new_app != $this->app) // are we changeing the app, or hit the user just write { $this->app = $new_app; // if we change init the data empty $this->data = array(); } return False; // continue edit } $content = array( 'app' => $posted_app, 'table' => $posted_table, 'version' => $this->setup_version($posted_app) ); $preserv = $content + array( 'new_app' => $new_app, 'new_table' => $new_table, 'edited_table' => $edited_table, 'changes' => $this->changes ); $new_version = explode('.',$content['version']); $minor = count($new_version)-1; $new_version[$minor] = sprintf('%03d',1+$new_version[$minor]); $content['new_version'] = implode('.',$new_version); $tmpl = new etemplate('etemplate.db-tools.ask_save'); if (!file_exists(PHPGW_SERVER_ROOT."/$posted_app/setup/tables_current.inc.php")) { $tmpl->disable_cells('version'); $tmpl->disable_cells('new_version'); } $tmpl->exec('etemplate.db_tools.needs_save',$content,array(),array(),$preserv); return True; // dont continue in edit } /*! @function has_single_index @syntax has_single_index( $col,$index ) @author ralfbecker @abstract checks if there is an index (only) on $col (not a multiple index incl. $col) @param $col column name @param $index ix or uc array of table-defintion @result True if $col has a single index */ function has_single_index($col,$index,&$options) { foreach($index as $in) { if ($in == $col || is_array($in) && $in[0] == $col && !isset($in[1])) { if ($in != $col && isset($in['options'])) { foreach($in['options'] as $db => $opts) { $options[] = $db.'('.(is_array($opts)?implode(',',$opts):$opts).')'; } $options = implode(', ',$options); } return True; } } return False; } /*! @function table2content @syntax table2content( $table ) @author ralfbecker @abstract creates content-array from a $table @param $table table-definition, eg. $phpgw_baseline[$table_name] @result content-array */ function table2content($table,&$columns,$extra_index=False) { $content = $columns = array(); for ($n = 1; list($col_name,$col_defs) = each($table['fd']); ++$n) { $col_defs['name'] = $col_name; $col_defs['pk'] = in_array($col_name,$table['pk']); $col_defs['uc'] = $this->has_single_index($col_name,$table['uc'],$col_defs['options']); $col_defs['ix'] = $this->has_single_index($col_name,$table['ix'],$col_defs['options']); $col_defs['fk'] = $table['fk'][$col_name]; if (isset($col_defs['default']) && $col_defs['default'] == '') { $col_defs['default'] = is_int($col_defs['default']) ? '0' : "''"; // spezial value for empty, but set, default } $col_defs['notnull'] = isset($col_defs['nullable']) && !$col_defs['nullable']; $col_defs['n'] = $n; $content["Row$n"] = $col_defs; $columns[$n] = $col_name; } $n = 2; foreach(array('uc','ix') as $type) { foreach($table[$type] as $index) { if (is_array($index) && isset($index[1])) // multicolum index { $content['Index'][$n]['unique'] = $type == 'uc'; $content['Index'][$n]['n'] = $n - 1; foreach($index as $col) { $content['Index'][$n][] = array_search($col,$columns); } ++$n; } } } if ($extra_index) { $content['Index'][$n]['n'] = $n-1; } if ($this->debug >= 3) { echo "

table2content(,,'$extra_index'): content ="; _debug_array($content); echo "

columns ="; _debug_array($columns); } return $content; } /*! @function content2table @syntax content2table( $content ) @author ralfbecker @abstract creates table-definition from posted content @param $content posted content-array @note It sets some reasonalbe defaults for not set precisions (else setup will not install) @result table-definition */ function content2table($content) { if (!is_array($this->data)) { $this->read($content['posted_app'],$this->data); } $old_cols = $this->data[$posted_table = $content['posted_table']]['fd']; $this->changes = $content['changes']; $table = array(); $table['fd'] = array(); // do it in the default order of tables_* $table['pk'] = array(); $table['fk'] = array(); $table['ix'] = array(); $table['uc'] = array(); for (reset($content),$n = 1; isset($content["Row$n"]); ++$n) { $col = $content["Row$n"]; while ((list($old_name,$old_col) = @each($old_cols)) && $this->changes[$posted_table][$old_name] == '**deleted**') ; if (($name = $col['name']) != '') // ignoring lines without column-name { if ($col['name'] != $old_name && $n <= count($old_cols)) // column renamed --> remeber it { $this->changes[$posted_table][$old_name] = $col['name']; //echo "

content2table: $posted_table.$old_name renamed to $col[name]

\n"; } if ($col['precision'] <= 0) { switch ($col['type']) // set some defaults for precision, else setup fails { case 'float': case 'int': $col['precision'] = 4; break; case 'char': $col['precision'] = 1; break; case 'varchar': $col['precision'] = 255; break; } } while (list($prop,$val) = each($col)) { switch ($prop) { case 'default': case 'type': // selectbox ensures type is not empty case 'precision': case 'scale': // case 'nullable': if ($val != '' || $prop == 'nullable') { $table['fd'][$name][$prop] = $prop=='default'&& $val=="''" ? '' : $val; } break; case 'notnull': if ($val) { $table['fd'][$name]['nullable'] = False; } break; case 'pk': case 'uc': case 'ix': if ($val) { if ($col['options']) { $opts = array(); foreach(explode(',',$col['options']) as $opt) { list($db,$opt) = split('[(:)]',$opt); $opts[$db] = is_numeric($opt) ? intval($opt) : $opt; } $table[$prop][] = array( $name, 'options' => $opts ); } else { $table[$prop][] = $name; } } break; case 'fk': if ($val != '') { $table['fk'][$name] = $val; } break; } } $num2col[$n] = $col['name']; } } foreach($content['Index'] as $n => $index) { $idx_arr = array(); foreach($index as $key => $num) { if (is_numeric($key) && $num && @$num2col[$num]) { $idx_arr[] = $num2col[$num]; } } if (count($idx_arr) && !isset($content['delete_index'][$n])) { if ($index['unique']) { $table['uc'][] = $idx_arr; } else { $table['ix'][] = $idx_arr; } } } if ($this->debug >= 2) { echo "

content2table: table ="; _debug_array($table); echo "

changes = "; _debug_array($this->changes); } return $table; } /*! @function read @syntax read( $app,&$phpgw_baseline ) @author ralfbecker @abstract includes $app/setup/tables_current.inc.php @param $app application name @param $phpgw_baseline where to put the data @result True if file found, False else */ function read($app,&$phpgw_baseline) { $file = PHPGW_SERVER_ROOT."/$app/setup/tables_current.inc.php"; $phpgw_baseline = array(); if ($app != '' && file_exists($file)) { include($file); } else { return False; } if ($this->debug >= 5) { echo "

read($app): file='$file', phpgw_baseline ="; _debug_array($phpgw_baseline); } return True; } function write_array($arr,$depth,$parent='') { if (in_array($parent,array('pk','fk','ix','uc'))) { $depth = 0; } if ($depth) { $tabs = "\n"; for ($n = 0; $n < $depth; ++$n) { $tabs .= "\t"; } ++$depth; } $def = "array($tabs".($tabs ? "\t" : ''); $n = 0; foreach($arr as $key => $val) { if (!is_int($key)) { $def .= "'$key' => "; } if (is_array($val)) { $def .= $this->write_array($val,$parent == 'fd' ? 0 : $depth,$key); } else { if (!$only_vals && $key === 'nullable') { $def .= $val ? 'True' : 'False'; } else { $def .= "'$val'"; } } if ($n < count($arr)-1) { $def .= ",$tabs".($tabs ? "\t" : ''); } ++$n; } $def .= "$tabs)"; return $def; } /*! @function write @syntax write( $app,$phpgw_baseline ) @author ralfbecker @abstract writes tabledefinitions $phpgw_baseline to file /$app/setup/tables_current.inc.php @param $app app-name @param $phpgw_baseline tabledefinitions @return True if file writen else False */ function write($app,$phpgw_baseline) { $file = PHPGW_SERVER_ROOT."/$app/setup/tables_current.inc.php"; if (file_exists($file) && ($f = fopen($file,'r'))) { $header = fread($f,filesize($file)); if ($end = strpos($header,');')) { $footer = substr($header,$end+3); // this preservs other stuff, which should not be there } $header = substr($header,0,strpos($header,'$phpgw_baseline')); fclose($f); if (is_writable(PHPGW_SERVER_ROOT."/$app/setup")) { $old_file = PHPGW_SERVER_ROOT . "/$app/setup/tables_current.old.inc.php"; if (file_exists($old_file)) { unlink($old_file); } rename($file,$old_file); } while ($header[strlen($header)-1] == "\t") { $header = substr($header,0,strlen($header)-1); } } if (!$header) { $header = $this->setup_header . "\n\n"; } if (!is_writeable(PHPGW_SERVER_ROOT."/$app/setup") || !($f = fopen($file,'w'))) { return False; } $def .= "\t\$phpgw_baseline = "; $def .= $this->write_array($phpgw_baseline,1); $def .= ";\n"; fwrite($f,$header . $def . $footer); fclose($f); return True; } /*! @function setup_version @syntax setup_version( $app,$new = '',$tables='' ) @author ralfbecker @abstract reads and updates the version and tables info in file $app/setup/setup.inc.php @param $app the app @param $new new version number to set, if $new != '' @param $tables new tables to include, if $tables != '' @return the version or False if the file could not be read or written */ function setup_version($app,$new = '',$tables='') { //echo "

etemplate.db_tools.setup_version('$app','$new','$tables')

\n"; $file = PHPGW_SERVER_ROOT."/$app/setup/setup.inc.php"; if (file_exists($file)) { include($file); } if (!is_array($setup_info[$app]) || !isset($setup_info[$app]['version'])) { return False; } if (($new == '' || $setup_info[$app]['version'] == $new) && (!$tables || $setup_info[$app]['tables'] && "'".implode("','",$setup_info[$app]['tables'])."'" == $tables)) { return $setup_info[$app]['version']; // no change requested or not necessary } if ($new == '') { $new = $setup_info[$app]['version']; } if (!($f = fopen($file,'r'))) { return False; } $fcontent = fread($f,filesize($file)); fclose ($f); if (is_writable(PHPGW_SERVER_ROOT."/$app/setup")) { $old_file = PHPGW_SERVER_ROOT . "/$app/setup/setup.old.inc.php"; if (file_exists($old_file)) { unlink($old_file); } rename($file,$old_file); } $fnew = eregi_replace("(.*\\$"."setup_info\\['$app'\\]\\['version'\\][ \\t]*=[ \\t]*')[^']*('.*)","\\1$new"."\\2",$fcontent); if ($tables != '') { if ($setup_info[$app]['tables']) // if there is already tables array, update it { $fnew = eregi_replace("(.*\\$"."setup_info\\['$app'\\]\\['tables'\\][ \\t]*=[ \\t]*array\()[^)]*","\\1$tables",$fwas=$fnew); if ($fwas == $fnew) // nothing changed => tables are in single lines { $fwas = explode("\n",$fwas); $fnew = $prefix = ''; $stage = 0; // 0 = before, 1 = in, 2 = after tables section foreach($fwas as $line) { if (eregi("(.*\\$"."setup_info\\['$app'\\]\\['tables'\\]\\[[ \\t]*\\][ \\t]*=[ \\t]*)'",$line,$parts)) { if ($stage == 0) // first line of tables-section { $stage = 1; $prefix = $parts[1]; } } else // not in table-section { if ($stage == 1) // first line after tables-section ==> add it { $tables = explode(',',$tables); foreach ($tables as $table) { $fnew .= $prefix . $table . ";\n"; } $stage = 2; } if (strpos($line,'?>') === False) // dont write the closeing tag { $fnew .= $line . "\n"; } } } } } else // add the tables array { if (strstr($fnew,'?>')) // remove a closeing tag { $fnew = str_replace('?>','',$fnew); } $fnew .= "\t\$setup_info['$app']['tables'] = array($tables);\n"; } } if (!is_writeable(PHPGW_SERVER_ROOT."/$app/setup") || !($f = fopen($file,'w'))) { return False; } fwrite($f,$fnew); fclose($f); return $new; } /*! @function update @syntax update( $app,$current,$version ) @author ralfbecker @abstract updates file /$app/setup/tables_update.inc.php to reflect changes in $current @param $app app-name @param $current new tabledefinitions @param $version new version @return True if file writen else False */ function update($app,$current,$version) { //echo "

etemplate.db_tools.update('$app',...,'$version')

\n"; if (!is_writable(PHPGW_SERVER_ROOT."/$app/setup")) { return False; } $file_baseline = PHPGW_SERVER_ROOT."/$app/setup/tables_baseline.inc.php"; $file_current = PHPGW_SERVER_ROOT."/$app/setup/tables_current.inc.php"; $file_update = PHPGW_SERVER_ROOT."/$app/setup/tables_update.inc.php"; if (!file_exists($file_baseline) && !copy($file_current,$file_baseline)) { //echo "

Can't copy $file_current to $file_baseline !!!

\n"; return False; } $old_version = $this->setup_version($app); $old_version_ = str_replace('.','_',$old_version); if (file_exists($file_update)) { $f = fopen($file_update,'r'); $update = fread($f,filesize($file_update)); $update = str_replace('?>','',$update); fclose($f); $old_file = PHPGW_SERVER_ROOT . "/$app/setup/tables_update.old.inc.php"; if (file_exists($old_file)) { unlink($old_file); } rename($file_update,$old_file); } else { $update = $this->setup_header; } $update .= " \$test[] = '$old_version'; function $app"."_upgrade$old_version_() {\n"; $update .= $this->update_schema($app,$current,$tables); $update .= "\n \$GLOBALS['setup_info']['$app']['currentver'] = '$version'; return \$GLOBALS['setup_info']['$app']['currentver']; } ?".">\n"; if (!($f = fopen($file_update,'w'))) { //echo "

Cant open '$update' for writing !!!

\n"; return False; } fwrite($f,$update); fclose($f); $this->setup_version($app,$version,$tables); return True; } function remove_from_array(&$arr,$value) { foreach($arr as $key => $val) { if ($val == $value) { unset($arr[$key]); } } } function update_schema($app,$current,&$tables) { $this->read($app,$old); $tables = ''; foreach($old as $name => $table_def) { if (!isset($current[$name])) // table $name droped { $update .= "\t\t\$GLOBALS['phpgw_setup']->oProc->DropTable('$name');\n"; } else { $tables .= ($tables ? ',' : '') . "'$name'"; $new_table_def = $table_def; foreach($table_def['fd'] as $col => $col_def) { if (!isset($current[$name]['fd'][$col])) // column $col droped { if (!isset($this->changes[$name][$col]) || $this->changes[$name][$col] == '**deleted**') { unset($new_table_def['fd'][$col]); $this->remove_from_array($new_table_def['pk'],$col); $this->remove_from_array($new_table_def['fk'],$col); $this->remove_from_array($new_table_def['ix'],$col); $this->remove_from_array($new_table_def['uc'],$col); $update .= "\t\t\$GLOBALS['phpgw_setup']->oProc->DropColumn('$name',"; $update .= $this->write_array($new_table_def,2).",'$col');\n"; } else // column $col renamed { $new_col = $this->changes[$name][$col]; $update .= "\t\t\$GLOBALS['phpgw_setup']->oProc->RenameColumn('$name','$col','$new_col');\n"; } } } if (is_array($this->changes[$name])) { foreach($this->changes[$name] as $col => $new_col) { if ($new_col != '**deleted**') { $old[$name]['fd'][$new_col] = $old[$name]['fd'][$col]; // to be able to detect further changes of the definition unset($old[$name]['fd'][$col]); } } } } } foreach($current as $name => $table_def) { if (!isset($old[$name])) // table $name added { $tables .= ($tables ? ',' : '') . "'$name'"; $update .= "\t\t\$GLOBALS['phpgw_setup']->oProc->CreateTable('$name',"; $update .= $this->write_array($table_def,2).");\n"; } else { $old_norm = $this->normalize($old[$name]); $new_norm = $this->normalize($table_def); foreach($table_def['fd'] as $col => $col_def) { if (($add = !isset($old[$name]['fd'][$col])) || // column $col added serialize($old_norm['fd'][$col]) != serialize($new_norm['fd'][$col])) // column definition altered { $update .= "\t\t$"."GLOBALS['phpgw_setup']->oProc->".($add ? 'Add' : 'Alter')."Column('$name','$col',"; $update .= $this->write_array($col_def,2) . ");\n"; } } } } if ($this->debug) { echo "

update_schema($app, ...) =

$update
)

\n"; } return $update; } /*! @function normalize_index @abstract orders the single-colum-indices after the columns and the multicolunm ones bedind @syntax normalize_index( $index,$cols ) @param index array with indices @param cols array with column-defs (col-name in key) @author ralfbecker @result the new array */ function normalize_index($index,$cols) { $normalized = array(); foreach($cols as $col => $data) { foreach($index as $n => $idx) { if ($idx == $col || is_array($idx) && $idx[0] == $col && !isset($idx[1])) { $normalized[] = isset($idx['options']) ? $idx : $col; unset($index[$n]); break; } } } foreach($index as $idx) { $normalized[] = $idx; } return $normalized; } /*! @function normalize @syntax normalize( $table ) @author ralfbecker @abstract sets all nullable properties to True or False @result the new array */ function normalize($table) { $all_props = array('type','precision','nullable','default'); foreach($table['fd'] as $col => $props) { $table['fd'][$col] = array( 'type' => ''.$props['type'], 'precision' => 0+$props['precision'], 'scale' => 0+$props['scale'], 'nullable' => !isset($props['nullable']) || !!$props['nullable'], 'default' => ''.$props['default'] ); } return array( 'fd' => $table['fd'], 'pk' => $table['pk'], 'fk' => $table['fk'], 'ix' => $this->normalize_index($table['ix'],$table['fd']), 'uc' => $this->normalize_index($table['uc'],$table['fd']) ); } /*! @function tables_identical @syntax tables_identical( $old,$new ) @author ralfbecker @abstract compares two table-definitions @result True if they are identical or False else */ function tables_identical($a,$b) { $a = serialize($this->normalize($a)); $b = serialize($this->normalize($b)); //echo "

checking if tables identical = ".($a == $b ? 'True' : 'False')."
\n"; //echo "a: $a
\nb: $b

\n"; return $a == $b; } };