📜 ⬆️ ⬇️

Facing refucktoring



The story of one refactoring, or the tale of how not to develop in PHP ...

It all started as always, from somewhere the customer appears all in tears and trance, yelling not in his own voice: “Save, help, my project slows down, users run from me. You have a week ... "

We deployed a project on our server - n-yes ... Then there was an analysis of this “Q” creation (those who follow me know what kind of Ukrainian company we are talking about), laughter through tears, and hysterics during the week. Well, for starters, XDEBUG was taken and looked at what was there and how (clickable):

')
Such a cool thing draws Webgrind , conveniently and clearly


Those. We have a page that displays a greeting + language selection + selection of a category for viewing (hard by the way) generated in 4.6 seconds on a server with a Core2 Quad CPU Q6600@2.40GHz/16Gb (hysteria and rays of hate to the Q office).

Much more terrible picture awaited us on the main page:


Before applying Zend Framework, we were separated by a few more weeks of optimizing the g ... code ...

FastTemplate such “Fast”



If you take a closer look, then on the previous screen you can see that a certain function parse_body is very expensive for us:



Inside waiting for us:



We look at the code:
// $ content - template
// $ rec - variables
$ content = preg_replace ( "/ \\ $ ([AZ] [A-Z0-9 _] +) /" , "@ \\ 1 @" , $ content ) ;
if ( is_array ( $ rec ) ) {
foreach ( $ rec as $ key => $ value ) {
$ name = strtoupper ( $ key ) ;
$ content = str_replace ( "@ $ name @" , " $ value " , " $ content " ) ;
}
}
return $ content ;


Those. at the very beginning we are looking for something like $ HTTP_PATH in templates, we replace it with @ HTTP_PATH @, then we try to replace all known variables with enumeration. There is one problem - we have a lot of templates, they use quite a bit of a couple of hundred variables. A simple replacement for str_replace with preg_replace_callback gave an increase of a couple of seconds (about 10%).

Code examples


Nervous better to skip this paragraph.

Classics of bad PHP code, found among Indians and our students:
function n ( ) {
global $ db , $ CONSTANTS , $ user , ...; // many in one word

echo $ CONSTANTS [ HOMEPAGE_BANNER1_ID ] ; // think this is a constant?
echo $ CONSTANTS [ HOMEPAGE_BANNER2_ID ] ; // do you know what's inside?
echo $ CONSTANTS [ HOMEPAGE_BANNER3_ID ] ; // 1, 2, 3, which do not change at all where O_o is declared
}


Using the file system instead of the version control system:
 tpl
 | - index.tpl
 | - index2.tpl
 | - index3.tpl
 | - index __. tpl
 `- index.44.tpl


With the database, the same number - table categories_old, items_1, etc.

If a boy likes work
pokes a finger in the book,
write about this here:
he's a good boy.


This, as you understand, is not about our “boys,” our 9,000 notices have on the main one. And only weakak use manuals:
// we do not read manuals
while ( $ row = mysql_fetch_array ( $ res ) ) {
if ( $ row ) {
foreach ( $ row AS $ key => $ field ) {
if ( ereg ( "^ [0-9] +" , $ key ) ) {
unset ( $ row [ $ key ] ) ;
}
}
}
$ rows [ ] = $ row ;
}
// if you finish a little
// trifle, of course, but an increase of ~ 0.1 sec. 23162 calls occur
while ( $ row = mysql_fetch_array ( $ res , MYSQL_ASSOC ) ) {
$ rows [ ] = $ row ;
}


Further, just a brilliant decision, I didn’t master the secret meaning of this creation:
$ sqls [ ] = $ sql ;
if ( is_array ( $ sqls ) ) {
foreach ( $ sqls AS $ ssql ) {
if ( $ ssql ) {
$ res = mysql_db_query ( $ db [ 'name' ] , $ ssql , $ db [ 'id' ] ) ;
if ( ! $ res ) {
return 0 ;
}
}
}
} else {
return 0 ;
}


Counting search results, what could be simpler:
// before the query, you can count the number of results
// using the db_count function (called in 96 different places)
function db_count ( $ sql ) {
global $ db ;
$ res = mysql_db_query ( $ db [ 'name' ] , $ sql , $ db [ 'id' ] ) ;
$ result = mysql_num_rows ( $ res ) ;
return $ result ;
}


Page navigation, and this too can:
// send a query to the database
$ res = mysql_db_query ( $ db [ 'name' ] , $ sql , $ db [ 'id' ] ) ;
// calculate the total (although db_count has already been called before)
$ row_count = mysql_num_rows ( $ res ) ;
// calculate how many pages we get
$ page_count = floor ( $ row_count / $ pager [ 'per_page' ] + 1 ) ;

// this is offset
$ bi = ( $ pos - 1 ) * $ pager [ 'per_page' ] ;

// now select only the necessary entries
for ( $ i = $ bi ; $ i < $ bi + $ pager [ "per_page" ] ; $ i ++ ) {
if ( $ i > = $ row_count ) {
break ;
}

if ( ! mysql_data_seek ( $ res , $ i ) ) {
break ;
}

if ( ! ( $ row = mysql_fetch_assoc ( $ res ) ) ) {
break ;
}
// store the result
$ new_rows [ ] = $ row ;
}


String repetition - we are not looking for easy ways (str_repeat):
// in file categories.sql.php
// function that builds select for HTML
$ offset_string = '' ;
for ( $ i = 1 ; $ i < $ rec [ 'level' ] ; $ i ++ ) {
$ offset_string . = '& nbsp; & nbsp; & nbsp; & nbsp;' ;
}


If we need to cut the string into 100 characters, and at the same time not shred the words, then here is the solution:
$ data [ 'row' ] [ 'description' ] = substr ( $ description , 0 , 100 ) ;

$ i = 100 ;
while ( ! ( $ description [ $ i ] == "" || $ description [ $ i ] == "_" ) && $ i < strlen ( $ description ) ) :
$ data [ 'row' ] [ 'description' ] . = $ description [ $ i ] ;
$ i ++;
endwhile ;

if ( $ i < strlen ( $ description ) ) {
$ data [ 'row' ] [ 'description' ] . = "..." ;
}


We have so many global variables, of course there is $ db, and it is passed to all functions that work with the database:
function db_query ( $ db , $ sql ) { }
function db_sql_query ( $ db , $ sql ) { }
function db_count ( $ db , $ sql ) { }
// etc.
// but why not, because we have one database
function db_query ( $ sql ) {
global $ db ;
}


Let the sea be rolling, but we will always save workarounds:
$ sql = "SELECT id, name, pasw from users where name = ' $ _POST [username] '" ;


About aggregation in SQL, we do not know:
// $ rows - records from the database
foreach ( $ rows as $ value ) {
$ total + = $ value [ "price" ] ;
}


Need a SEO URL? No problem:
switch ( $ params [ 1 ] ) :
case "usageagreement" :
$ page_id = 13 ;
break ;
case "privacypolicy" :
$ page_id = 14 ;
break ;
case "termsandconditions" :
$ page_id = 15 ;
break ;
case "affiliates" :
$ page_id = 22 ;
break ;
case "aboutus" :
$ page_id = 19 ;
break ;
endswitch ;


Ok with PHP, but HTML could be learned:
<! - id is such id ->
< div id = "banner" > ... < / div >
< div id = "banner" > ... < / div >
< div id = "banner" > ... < / div >
< div id = "banner" > ... < / div >

<! - class is almost style ->
< li class = "padding-left: 15px;" > ... < / li >

<! - table layout ->
<! - although it is not necessary to paint 10 nested tables ->

<! - margin, what is a margin? ->
& nbsp; & nbsp; & nbsp; & nbsp; & nbsp; & nbsp;


Application Zend_Cache


Kesh will save the world, we thought and screwed it for all SQL queries (good, someone guessed to write a single function db_sql_query ) and all calls to parse_body . For starters, we tried to cache files, it helped on the test server, but not live. The reason is that we have so many small templates (~ 200 for the main one) that operations with the file system negated the caching gain.

The second attempt was more successful, we decided to use memcache - a speed increase of ~ 180%. What a cool indicator, but true only in 100% hit in the cache, so before us loomed the prospect of a complete refactoring of the system.

Some client optimization


Well, what can I tell you, simply adding the following rules to .htaccess greatly facilitated navigation for users who have visited the site at least once:

FileETag MTime Size

AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/x-javascript

<ifModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 seconds"
ExpiresByType text/html "access plus 1 seconds"
ExpiresByType image/x-icon "access plus 2592000 seconds"
ExpiresByType image/gif "access plus 2592000 seconds"
ExpiresByType image/jpeg "access plus 2592000 seconds"
ExpiresByType image/png "access plus 2592000 seconds"
ExpiresByType text/css "access plus 604800 seconds"
ExpiresByType text/javascript "access plus 216000 seconds"
ExpiresByType application/x-javascript "access plus 216000 seconds"


And then everything is in order:


Zend Framework


And now I’ll talk about how the project is slowly moving to the Zend Framework. It all starts with a simple check in index.php (oh yeah, there’s one entry point in our system):

// list of modules that have already been refactored
$ modules = array (
'/ search /' ,
'/ about /'
) ;

$ path = $ _SERVER [ 'REQUEST_URI' ] ;

// we are satisfied with such a simple check
// but a request of the form / search /? ... will no longer be processed
if ( in_array ( $ path , $ modules ) ) {
// connect ZF (inside the standard code from generated public / index.php)
require 'loader.php' ;
exit ( ) ;
}

// and this one will be
foreach ( $ modules as $ module ) {
if ( strpos ( $ path , $ module ) === 0 ) {
require 'loader.php' ;
exit ( ) ;
}
}


If we have more than one entry point, then in each file that were affected we make a simple insert:
$ _SERVER [ 'REQUEST_URI' ] = str_replace ( $ _SERVER [ 'PHP_SELF' ] , '/ admin /' , $ _SERVER [ 'REQUEST_URI' ] ) ;
require 'loader.php' ;
exit ( ) ;


In order to forget the "global" horror, the vital variables were thrown into Zend_Registry (and later thrown into the application.ini configuration file, where they belong).

Also Zend_Translate was fed a table with translations (see adapter array)

Result


It is difficult to judge the result, the project is in development, but here are a few comparative measurements:

Generation timeGeneration time (re)Generation time (ZF)Page sizePage Size (ZF)
Home page4 663ms2 759ms-699.5Kb288.0Kb
Static pages3 115ms2 008ms295ms263.3Kb166.2Kb
Page item3,082ms1,745ms180ms589.1Kb260.8Kb


Another vivid screenshot of the average / maximum time of page generation by dates: http://screencast.com/t/NDY1NGE5

UPDATE : Sorry for the tone of the article, but somehow boiling something. If it seems to someone that the purpose of the article is to show who is white and fluffy, and who else is not, the main idea is to share experience, show examples of “bad” code, and it does not matter who wrote it, the main thing is for everyone to learn lesson, and this code is becoming less ...

Source: https://habr.com/ru/post/102220/


All Articles