
So, you are inspired by the idea of a Web 2.0 startup. You think that you have come up with something original and fresh. You see an effective implementation of your idea. You believe that your project will revolutionize the market. If it is such thoughts that occupy you, it's time to do a business plan. Business planning is a separate discipline and you can find a lot of literature about it. However, if you do not have experience in drawing up business plans, it is better to resort to the help of professionals. The worse the business is predicted, the higher the risks of its collapse.
However, let us assume that you have an attractive business plan, you expect the project to be self-sustaining within 2 years from the moment it starts, and anticipate an annual 50% increase in its advertising potential. In MS-Visio prepared promising schemes of the user interface of the project. And even more, your designer, armed with
fashionable leadership , in a record time, marked the graphic look of the project. So, typical user interface pages, created in the best traditions of non-cash layout, wait for their high point in your local project folder.
Project outline
I can not know what the essence of your startup, what is its feature. But in order to build on a practical example, let's consider a simplified version of the beloved collective blog habrahabr.ru. Obviously, you hardly plan on repeating a famous project. You can follow a proven path and develop a photo blog in the style of flicr.com, social network a-la facebook.com, social bookmarks in the manner of
ma.gnolia.com or social news in the traditions of digg.com. Perhaps you choose your own winding path to commercial success. In any case, you will encounter approaches common for Web 2.0 projects, such as commenting, rating, tagging, pop-up contextual tooltips, etc. As for the tape articles or, say, the user panel - these solutions are valid for virtually any project.

')
So, in our example there are only a few informational pages (“About the Site”, “Help”), an article feed, a user panel (“Registration”, “Authorization”), a tag cloud associated with an article feed, and a feed of recent comments.
Suppose we would like the iterations with the forms (registration, authorization, adding a comment), if possible, not to overload the web page. System messages appeared in a given design and supported the principle of “dragged and left” (Drag & Drop). When executing processes, the system should report on what and how long it is busy.
I think now the task is clear, it remains to think about its implementation.
Platform selection
Just take and start programming - the idea is not the best. We need guarantees that at a certain stage we don’t have to start everything from the beginning, that increasing attendance will not be a problem, that “endless beta” will not turn into “endless alpha” and that user requests for new widgets or the need for “open API” will not you stumbling block. It would be good to rely on someone else's experience, on a proven approach. The solution here can be the use of one of the popular frameworks:
Zend Framework ,
Prado ,
CakePHP ,
Symphony Project ,
Seagull Framework ,
WACT ,
PHP on TRAX ,
ZooP Framework ,
eZ Components or
CodeIgniter .
Each has its strengths. For example, the performance of the Zend Framework is a completely logical advantage for a solution from PHP developers. Prado - distinguishes a XAML-like model for declaring user interfaces. Apparently, CakePHP is famous for its greatest flexibility and scalability. However, all frameworks have common properties.
All frameworks allow you to use different DBMS, without requiring a code change . All of them support PHP5 (but not all are capable of working on PHP4). All of them contain data validation components. And almost all are built on the model of MVC (Model-View-Controller). If you already have experience with one of these frameworks, I will not prevent you from using your favorite platform. However, if you are considering this list now, not knowing where to start, I rush to remind you that any universal solution (and all these frameworks are quite universal) cannot compare in performance with a well-executed partial solution designed to solve only one given task. . Well, let's try to create our own software platform for solving problems of our startup. So, we find a hosting plan with PHP 5.2 and MySQL 4/5. Do not forget to make sure that this plan includes the PHP PDO (
PHP Data Objects ) extension. Using this library of abstract database access will allow you to easily switch to another DBMS if necessary.
Component model
Now is the time to decide on the component model (the principle of placing project scripts). With a successful component model, you will always quickly find the required scripts, it will be easy to navigate the system during debugging, you will not have problems with the development of the project.
So, in the root folder of the project will be located
index. php , which must accept all project requests, connect the required libraries and transfer control to the appropriate scripts. The
app folder will contain scripts that ensure the performance of various project web pages. The scripts will be distributed in folders
controllers ,
models ,
views . This is the same MVC model. For any web page, the system will find the corresponding model script and take in it all the data necessary for this interface. Then the corresponding web page of the view script will be called up, which will process this data for output. If data were sent to the web page in POST or GET, before the model and view scripts, the controller script will be called up, which will perform the required actions with the data that has become dull. In addition to the MVC folders, I suggest also to get the
ajax_ controllers folder, where the controllers for asynchronous Java Script requests (AJAX) will be located.

Go back to the root folder and create a classic set of folders for the design of the project and JS scripts (
css / images / js ). Create a
config pack where the
config file will be stored
. inc. php . In it, we define the interface and data for access to the database, the HTTP_PATH and ROOT_PATH project constants, constants with the names of the database tables and other configuration data. The
libs folder will contain software libraries. Files uploaded by users will be sent to the usercontent folder. Third-party libraries and solutions such as
FCKEditor ,
LastRSS ,
MediaPlayers ,
YUI will be placed in the vendors folder.
How does this scheme work in practice? Let's take an example. The user has requested the web page
nashayt / blog .
The .htaccess file in the root folder forwarded the request to index.php
.htaccess
DirectoryIndex index.php
ErrorDocument 404/404 /
Options + Followsymlinks
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond% {REQUEST_FILENAME}! -F
RewriteCond% {REQUEST_FILENAME}! -D
RewriteRule ^ (. *) $ Index.php?% {QUERY_STRING} [L]
</ IfModule>
Index.php reads the configuration from config.php, includes a small set of commonly used functions, such as the
debugging function toLog () . Then the pattern set (
patterns. Inc. Php ) is turned on and the environment is initialized (
init. Inc. Php ). In the simplest case, during the initialization, the query string $ _SERVER ['REQUEST_URI'] is analyzed and the variables $ CTRLPATH, $ RECORD_ID, and $ APPPATH are assigned based on its content. How? In the case of a request, our
site / controller / address - $ CTRLPATH takes the value "address". In the case of a query, our
site / address / 000023 (regular expression mask / \ d {7} /) - $ RECORD_ID takes the value 23. $ APPPATH - always takes the value “address”.
index.php
<?
if (preg_match ("/ \. (gif | jpg | bmp | js | css) \ /? $ / is", $ _SERVER ['REQUEST_URI'])) exit;
include (ROOT_PATH. "config / config.inc.php");
include (ROOT_PATH. "app / basics.inc.php");
include (ROOT_PATH. "app / patterns.inc.php");
include (ROOT_PATH. "app / init.inc.php");
$ db = Lib :: factory ('db');
$ db-> applyAuthorization ();
// Get the controller
if ($ CTRLPATH) {
// If there is a cache image of the answer
if (file_exists (ROOT_PATH. "cache /". md5 ("ajax_controllers {$ CTRLPATH}"). ". tmp"))
$ OUT = file_get_contents (ROOT_PATH. "Cache /".
md5 ("ajax_controllers {$ CTRLPATH}"). ". tmp");
else {
$ ctrl = Lib :: factory ('controller');
include (ROOT_PATH. "app / ajax_controllers {$ CTRLPATH} index.inc.php");
}
} else {
// If there is a cache image of the page, show it
if (file_exists (ROOT_PATH. "cache /". md5 ($ APPPATH). ". tmp"))
$ OUT = file_get_contents (ROOT_PATH. "Cache /". Md5 ($ APPPATH). ". Tmp");
else {
// Get the webpage
$ InterfaceScript = ($ RECORD_ID? "Record": "index"). ". Inc.php";
include (ROOT_PATH. "app / controllers / common.inc.php");
if (file_exists (ROOT_PATH. "app / controllers {$ APPPATH}". $ InterfaceScript))
include (ROOT_PATH. "app / controllers {$ APPPATH}". $ InterfaceScript);
include (ROOT_PATH. "app / models / common.inc.php");
if (file_exists (ROOT_PATH. "app / models {$ APPPATH}". $ InterfaceScript))
include (ROOT_PATH. "app / models {$ APPPATH}". $ InterfaceScript);
if (file_exists (ROOT_PATH. "app / views {$ APPPATH}". $ InterfaceScript))
include (ROOT_PATH. "app / views {$ APPPATH}". $ InterfaceScript);
else
include (ROOT_PATH. "app / views / _404 / index.inc.php");
}
}
header ("Content-type: text / html; charset = UTF-8");
print $ OUT;
if (isset ($ _ GET ["createcache"]))
file_put_contents (ROOT_PATH. "cache /". md5 ($ APPPATH). ". tmp", $ OUT);
?>
app / patterns.inc.php
<?
class lib {
// The parameterized factory method
public static function factory ($ type) {
if (include_once ROOT_PATH.'libs / '. $ type.' .lib.php ') {
$ classname = $ type;
return new $ classname;
} else {
throw new Exception ('Driver not found');
}
}
}
?>
Next, the system establishes a connection to the database and checks whether the user is authorized. Since the webpage was requested, the list entry will not be sequentially polled.
app / controllers / blog / index.inc.php
app / models / blog / index.inc.php
app / views / blog / index.inc.php
POST and GET do not contain data and, accordingly, the controller script will be skipped. In the app / models / blog / index.inc.php model script, the data for the list of articles will be accepted, and in the app / views / blog / index.inc.php script, this data will be displayed for output.
app / models / blog / index.inc.php
<?
$ CONTENT ["WINDOW_TITLE"] = "Startup: Blogs";
$ NAVIGATION ["limit"] = 5;
$ get = Lib :: factory ('get');
$ DATA ["MEDIALIST"] = $ get-> articleList ();
$ DATA ["PAGINATION"] = $ get-> pagination ();
?>
app / views / blog / index.inc.php
<?
$ CONTENT ["BODY"] = '';
foreach ($ DATA ["MEDIALIST"] as $ row) {
$ rec_url = HTTP_PATH. $ MEDIATYPES [$ row ["media_type"]] ["var"].
'/'.getIDUrl($rowrow""__"") ).'/';
$ CONTENT ["BODY"]. = '
<div class = "media_line">
<div class = "info"> Added (a) '. $ row ["fullname"].', viewed '.
(int) $ row ["visited"]. ' times </ div>
<div class = "text"> '. $ row ["description"].' </ div>
<div class = "footer"> '. ($ row ["cache_commentnumber"]?' <a href = "'.
$ rec_url. '">'. $ row [" cache_commentnumber "].
'comments </a>': '<a href = "'.
$ rec_url. '"> Add a comment </a>'). '
</ div>
</ div> ';
}
include (INCLUDEPATH. "header.inc.php");
$ OUT. = '
<div class = ”left_col”>
'. $ CONTENT ["BODY"]. $ CONTENT ["PAGINATION"]. '
</ div>
<div class = ”right_col”>
<div id = ”UserPanel”> </ div>
<div id = ”TagCloud”> </ div>
<div id = ”LastestComments”> </ div>
<script type = "text / javascript">
showBlock ("UserPanel");
showBlock ("TagCloud");
showBlock ("LastestComments");
</ script>
</ div>
';
include (INCLUDEPATH. "footer.inc.php");
?>
Perhaps you noticed a call to the get class to get a list of articles. Thanks to the factory pattern, we can load into memory only those libraries that we use in actual tasks. But I propose to go further and design the libraries so that for the most frequently requested web pages we are limited to minimal memory consumption. Let's put all the data query methods that will be required in each interface in the get class. Other methods can be divided into classes in accordance with the logic of our project. They will be received by the factory only as needed. For example, when you want to save the content of a user comment.
As can be seen from the view script, in this case a page will be displayed containing a
list of articles in the left column. In the right column are expected custom panel, tag cloud, recent comments. In the view script, while we can only see the containers of these panels. However, calls to the showBlock function will accept and insert the contents of the panels into these containers.
So we got a list of articles. Now you need to consider the case of a user request a separate article from this list. In this case, during the initialization of the environment, the variable $ RECORD_ID will be defined. The system will poll the universal model for any of the entries in the blog list. The model will be found at app / models / blog / record.inc.php. $ RECORD_ID will serve as a parameter to query the article data.
$ get-> article ($ RECORD_ID);
The system will look for the view script at app / views / blog / record.inc.php.
By analogy with the web page of the list of articles, when the user requests an
information page (for example, our
site / about ) in the model script, the page data will be received (app / models / about / index.inc.php), in the view script (app / views / about / index.inc.php) will be defined design.
We enrich the user interface
Under the terms of the task, the system should, if necessary, display messages in stylized floating windows, report on the status of processes. The easiest way is to use the panel component from the user interface library from Yahoo (
http://developer.yahoo.com/yui/container/panel/ ). However, if you want to save users of your project from having to wait for the download of an additional JS script of about 100Kb in size, you can try to make your own library. For system messages, you need the two functions showSystemMessage () and hideSystemMessage (). The first will show the hidden layer of the message box (document.getElementById ("window_id"). Style.display = "block") and place the message passed to the function (document.getElementById ("window_content_is"). InnerHTML = message). The second function will hide the message (document.getElementById ("window_id"). Style.display = "none"). It is also desirable to programmatically position the layer in the center of the browser window before showing the layer.
Great, now we can show the system message window and hide it. But how to enable Drag & Drop for it? It is enough just to specify event dispatchers for mouse operations on the window layer:
<div id = "window_id" onmousedown = "windowMouseDown ('window_id', event)" onmouseup = "windowMouseUp ('window_id')"> ... </ div>
and add to the project JS-scripts handling of these events.
js / panels.js
// Quick access to the object
function $ (divName) {return document.getElementById (divName); }
// Determine the type of browser
if (document.implementation && document.implementation.createDocument) var isMozilla = true;
else var isMozilla = false;
// Capture the object to move and its coordinates
function windowMouseDown (divNamePref, ev) {
if (isMozilla) {event = ev; }
currentWindowDivNamePref = divNamePref;
// Save offset
currentWindow [divNamePref] = {
"x": event.clientX + document.body.scrollLeft -
$ (divNamePref) .style.left.replace ("px", ""),
"y": event.clientY + document.body.scrollTop -
$ (divNamePref) .style.top.replace ("px", "")
};
}
// Move the object
function windowMouseMove (ev) {
if (! currentWindowDivNamePref) return false;
if (! currentWindow [currentWindowDivNamePref]) return false;
if (isMozilla) {event = ev; }
$ (currentWindowDivNamePref) .style.left = (event.clientX +
document.body.scrollLeft - currentWindow [currentWindowDivNamePref] .x) + "px";
$ (currentWindowDivNamePref) .style.top = (event.clientY +
document.body.scrollTop - currentWindow [currentWindowDivNamePref] .y) + "px";
return false;
}
// Release Object
function windowMouseUp (divNamePref) {
currentWindow [divNamePref] = null;
currentWindowDivNamePref = false;
}
if (isMozilla) {document.captureEvents (Event.MOUSEMOVE); }
document.onmousemove = windowMouseMove;
In the case of the process status window, below the window layer, a semitransparent layer is added, which is filled with gray and covers the content of the entire browser window. Thus we get the effect of a temporary "freezing" of the user interface. In the message window, you can add a dynamic image that visualizes the process. As a rule, the Close button is located in the layer code of the system message window. In the case of a process status window, this is not required.
Now we will consider a practical task that will require the described functions of managing the system windows. We need to implement a custom panel in the project. This is a component that allows project visitors to either register or log in. Those. when the user has filled out the registration form, the system should send his data to the server, analyze them and either report a filling error (say, if the Captcha confirmation code was entered incorrectly), or register the user and, again, report the result. We can now report something using the system message window. As you must already remember, in order for the system to simply display the user panel, we will need a JS request to the server (the showBlock () function). In order to verify the user input without overloading the web page, again, can not do without AJAX. Thus, we need a set of JS functions for sending asynchronous requests to the controller on the server and receiving controller responses. In this case, again, you can use your own solution, and you can rely on the experience of popular open source libraries. Let's take a look at using the Connection Manager component from YUI (
http://developer.yahoo.com/yui/connection/ ). To use it, you will have to call two scripts yahoo-min.js and connection-min.js in the code of the project pages.
Let's take a look at how the showBlock function works, displaying the supporting components of a web page.
js / common. js
// AJAX functions
// Quick access to the object
function $ (divName) {return document.getElementById (divName); }
// Send request to controller
function showBlock (BlockID) {
YAHOO.util.Connect.asyncRequest ('POST', "http: // our site / controller /" +
BlockID.toLowerCase () + "/",
callback, "ctrl_action = getComponent");
}
// Define the behavior of the system when receiving a response
var callback =
{
success: CtrlRespond,
failure: commonHandleFailure,
argument: ['foo', 'bar']
};
// Failed reply
var commonHandleFailure = function (o) {
if (o.responseText! == undefined) {
showSystemMessage ("Connection Error");
}
};
// Analysis of the controller response
var CtrlRespond = function (obj) {
if (obj.responseText == undefined) return false;
if (obj.responseText.substr (0,1) == "{") {
var respondStructure = eval ('(' + obj.responseText + ')');
// The controller asks for an error
if (respondStructure.ErrorMsg) return showSystemMessage ("ERROR:" +
respondStructure.ErrorMsg);
// The controller asks to perform the corresponding code action
if (respondStructure.ActionCode == 1) {$ (respondStructure.ID) .innerHTML =
respondStructure.Body; return true;
}
// The controller asks to show the message
if (respondStructure.Body) showSystemMessage (respondStructure.Body);
} else alert (obj.responseText); // debug in case of response
// with structure incorrect for JSON
};
app / ajax_controllers / userpanel / index.inc.php
<?
// Controller for user panel
class RD extends controller {
private $ user;
function __construct () {
$ this-> user = Lib :: factory ('user');
}
function getComponent () {
$ this-> ID = "UserPanel";
$ this-> ActionCode = 1;
$ this-> Body = "..Content panel ..";
}
}
$ rd = new RD ();
if (isset ($ _ POST ["ctrl_action"])) {
call_user_method ($ _ POST ["ctrl_action"], $ rd);
}
$ rd-> respond ();
?>
libs / controller.lib.php
<?
// Initial controller class
class controller {
public $ ActionCode = 1;
public $ ErrorMsg = "";
public $ Body = "";
public $ ID = "";
function respond ($ message = "", $ errormsg = "") {
if ($ message) $ this-> Body = $ message;
if ($ errormsg) $ this-> ErrorMsg = $ errormsg;
$ out = '{
"ActionCode": "'. $ This-> ActionCode.'",
"ID": "'. $ This-> ID.'",
"ErrorMsg": "'. ($ This-> ErrorMsg? Addslashes (preg_replace (" / [\ r \ n] / ",
"", $ this-> ErrorMsg)): ""). '",
"Body": "'. ($ This-> Body? Addslashes (preg_replace (" / [\ r \ n] / ",
"", $ this-> Body)): ""). '"
} ';
header ("Content-type: text / html; charset = UTF-8");
print $ out;
exit;
}
}
?>
When the instruction in the showBlock HTML web page (“UserPanel”) has completed, a request is sent to the app / ajax_controllers / userpanel / index.inc.php controller. The request parameter is specified in the ctrl_action request parameter. The controller returns a JSON structure with the variables ID, ActionCode, Body to the Java Script. The JS CtrlRespond () function analyzes the resulting variables. For ActionCode == 1, as in our case, it inserts the content obtained in BODY into the layer with the ID ID.
As soon as we get a custom panel, we can write a function to send data to the controller when registering a project visitor.
js / common.js
// Send data to the controller
function registerUser (obj) {
if (obj.login.value == '' || obj.password.value == '' ||
obj.email.value == '' || obj.fullname.value == '' || obj.gencode.value == ''
) showSystemMessage ('It is necessary to fill in all fields of the form');
else YAHOO.util.Connect.asyncRequest ('POST', "http: // our site / controller / userpanel /",
callback, "ctrl_action = createUser & login =" +
obj.login.value + "& password =" + obj.password.value +
"& email =" + obj.email.value + "& fullname =" +
obj.fullname.value + "& gencode =" + obj.gencode.value);
}
...
// Analysis of the controller response
var CtrlRespond = function (obj) {
...
if (respondStructure.ActionCode == 2) return showSystemMessage (respondStructure.Body);
...
};
app / ajax_controllers / userpanel / index.inc.php
<?
// Controller for user panel
class RD extends controller {
...
function createUser () {
if (! $ _ POST) {$ this-> ErrorMsg = "Required fields are not filled in"; return false; }
if (! $ this-> user-> add ($ data)) {$ this-> ErrorMsg =
"Error adding user"; return false; }
$ this-> ActionCode = 2;
$ this-> Body = "User created successfully";
}
...
}
?>
The user registration form contains the <input type = "button" value = "Join" button onclick = "registerUser (this)" />. When you click on it, the app / ajax_controllers / userpanel / index.inc.php controller is passed data from the form and the createUser () method is requested for processing. During the execution of the method, the ErrorMsg error message, the request command code on the client side of the ActionCode, or the body message text can be sent to the controller's response structure. In this case, if you successfully create a user, a corresponding message will appear, and in case of an error, a message about it will appear.
Similarly, we can write functions for requests to check the presence of the entered login or email in the database. They will request the methods of the controller class app / ajax_controllers / userpanel / index.inc.php checkLogin and checkEmail, respectively. The methods themselves can check and report the result in the BODY variable. By a similar principle, you can organize the display of the authorization form and the reaction to events in it. However, this applies to any components of the project user interface that require AJAX.
When displaying a tag cloud, you might need the following function.
Tag Cloud
<?
function cmp_tag ($ a, $ b) {
if ($ a ["tag"] == $ b ["tag"]) return 0;
return strcmp ($ a ["tag"], $ b ["tag"]);
}
function getClouds () {
global $ db;
$ lines = array ();
$ sizes = array ("x-small", "small", "medium", "large", "x-large");
$ sql = "SELECT * FROM" .TAGCLOUDINDEXTABLE. "LIMIT 0.20";
$ sth = $ db-> prepare ($ sql);
$ sth-> execute ();
$ result = $ sth-> fetchAll (PDO :: FETCH_ASSOC);
if (! $ result) return false;
$ indexes = array ();
$ tags = array ();
foreach ($ result as $ line) {
$ tags [trim ($ line ["tag"])] = $ line ["tag_index"];
$ indexes [] = $ line ["tag_index"];
}
$ min = min ($ indexes);
$ max = max ($ indexes);
$ range = ($ max- $ min);
foreach ($ tags as $ tag => $ index) {
$ lines [$ tag] ["tag"] = $ tag;
$ lines [$ tag] ["tag_index"] = $ index;
$ lines [$ tag] ["size"] = $ sizes [sprintf ("% d", ($ index- $ min) / $ range * 4)];
$ lines [$ tag] ["title"] = "Tag". $ tag. "found". $ index. ' times';
}
usort ($ lines, "cmp_tag");
return $ lines;
}
?>
To implement pop-up contextual prompts, you can use the same principle on which the system message windows were created, with the only difference that the prompts should be positioned directly at the position of the mouse cursor at the time of the click. The functions of positioning, displaying and caching prompts can be found in my library Thesaurus (
http://www.phpclasses.org/browse/package/3505.html )

Optimization
I am sure that you are counting on high attendance of the project under development. It means that it will not be superfluous to reduce the load on the server as much as possible.
Tell your server's scheduler (CRONTAB) “once every 30 minutes” to run the index.php script with the GET createcache parameter (/ usr / bin / php -f / fulladdress / index.php "& createcache = on"). The system will create a cache image for the main page every half hour. The highest frequency of hits usually falls on the main page. In our case, the project will return the previously prepared HTML, skipping relatively resource-intensive operations, accessing the database, etc. However, when the page loads in the user's browser, the Java Script will query the auxiliary components (the user panel and others). Controller responses can also be cached. It should be added to the controller analysis of GET variables:
if (isset ($ _ GET ["ctrl_action"])) {
call_user_method ($ _ GET ["ctrl_action"], $ rd);
}
Add a condition to create a cache image of the response to the method of the source class:
print $ out;
if (isset ($ _ GET ["createcache"]))
file_put_contents (ROOT_PATH. "cache /". md5 ("ajax_controllers {$ CTRLPATH}"). ". tmp", $ OUT);
exit;
Now, with a direct access to the controller, our
site / controller / tagcloud ? ctrl_action = getComponent & createcache = on will create a cache for its response.
It remains to assign the task scheduler to run the specified scripts with parameters (/ usr / bin / php -f / fulladdress / index.php "& ctrl_action = getComponent & createcache = on & request_uri = / controller / tagcloud /") at specified intervals (say, update the tag cloud every 2 hours). Just do not forget to make sure that the GET request_uri parameter in the environment initialization script app / init.inc.php is checked. If there is one, the system should use it instead of $ _SERVER ['REQUEST_URI'].
Conclusion
At this crazy time,
when Web 2.0 projects are valued at millions of dollars, even in Russia , the desire to try yourself in this market is understandable. The launch of the project to the market, its capitalization are business issues, not programming. However, to do this business, you need a product. This article is not a Web 2.0 project programming guide. The article contains material that can guide you if you are looking for a suitable way to develop a project. I also wanted to show here that, having gotten one can “revive” a typical Web 2.0 startup in a relatively short time. If you have an interesting original idea, then let the difficulties of its implementation do not stop you. Dare and, who knows, maybe your idea will really be in demand, maybe it will bring you more than you dare to hope. There are enough stories on the Internet for the startling success of Web 2.0 startups made by people who dare. Perhaps in time there will appear your success story. Successes! ..
Original article in PDF