by Emily Stamey
Application Developer at NC State University
Application Developer at NC State University
It's harder to read code than to write it. This is why code reuse is so hard.- Joel Spolsky
Web development involves supporting legacy applications
It is HARD to update a spaghetti codebase
It is also HARD to rewrite a large legacy application
Bootstrapping gives you flexibility to update your application
Let's review some terminology... (source: wikipedia)
Software developed using older technologies and practices. It can be difficult to replace because of its wide use.
Often a negative term
Referencing a system as "legacy" often implies that the system is out of date or in need of replacement.
The relationships between pieces of code are so tangled, it’s nearly impossible to add or change something without unpredictably breaking something.
Technique for restructuring an existing body of code, altering its internal structure without changing its external behavior.
A metaphor referring to the eventual consequences of any system design, software architecture or software development within a codebase.
Building onto an existing system for the purpose of improvement with the least amount of sweat equity and development cost in the process.
Talk to users of the application
Study the codebase
Examine the new feature requests
Talk to users of the application
Study the codebase
Examine the new feature requests
Is their process consistent with the application?
What are the pain points?
Do they have concerns with the application?
Don’t rely on developer feedback
Process was inconsistent with the application
Selection Committee used a spreadsheet of available scholarship money
Selected Candidates were added to a spreadsheet Student ID and Amount and Term of award
High Margin of Error
Their process was exiting and re-entering the system through spreadsheets
Many awards were rejected by Financial Aid
They didn’t trust that Selection Committee was choosing the best candidates
The Scoring algorithm was not clear/effective
Multiple majors weren't allowed by our system
NOT ALL MONEY WAS BEING AWARDED
MONEY LEFT UN-GIVEN ⇒ ANGRY DONORS
Talk to users of the application
Study the codebase
Examine the new feature requests
Talk to past/current developers
Verify that key functionality does what everyone thinks it does
Look for entanglements
Large App model
SQL queries, only slightly dynamic
Functions weren’t single-purpose
No Bounded Contexts between Students, Selection, and Foundation
Student application data was a single row in table
Academic information wasn’t updated when it changed
Major was a single column in that row
Study the codebase
Talk to users of the application
Examine the new feature requests
What new features are needed?
How they might be implemented?
Explicit criteria matching, excluded non-matching applicants
Current student data, query their GPA, Major, etc at time of selection
Students have multiple majors
Is there another application that can do what it does? Is it better?
Is this a worthwhile investment?
If so, what are the Most Valuable Features?
Explain why this work is necessary
Be open about errors in the application
Build trust from the beginning
Customer has an open door to you
add work from the side
change processes
Keep the door open anyway!
Bootstrapping grows FAST!
Large projects
require more people to be engaged in the process
wear people down over time
Scope the work you are agreeing to do
Divided the application based on timeline
we didn't scope the work
Version control
Stabilize the code base and preserves history
Development and Staging environments (w/ Fake data)
No more developing in production!!!!
Acceptance tests stabilize functionality you need
added hooks for testing interfaces
Codeception needed to view contents of the pages
Unit and Functional tests for everything you build
{
"name": "itecs/scholarships",
"description": "Scholarships application for the College of Engineering.",
"require": {
"php": ">=5.3.3",
"robmorgan/phinx": "*",
"pimple/pimple": "~3.0",
"ncsu/auth": "dev-master"
},
"require-dev": {
"codeception/codeception": "2.0.*"
}
...
}
{
"name": "itecs/scholarships",
"description": "Scholarships application for the College of Engineering.",
"require": {
"php": ">=5.3.3",
"robmorgan/phinx": "*",
"pimple/pimple": "~3.0",
"symfony/http-foundation": "2.5.*",
"illuminate/database": "4.1.*",
"illuminate/support": "4.1.*",
"ncsu/auth": "dev-master",
"twig/twig": "~1.16",
"mathiasverraes/money": "v1.3.0",
"monolog/monolog": "~1.12",
"mpdf/mpdf": "dev-master"
},
"require-dev": {
"codeception/codeception": "2.0.*",
"codeception/specify": "*",
"codeception/verify": "*",
"fzaninotto/faker": "1.4.*",
"phpunit/phpunit": "4.1.*",
"mockery/mockery": "0.9.*",
"symfony/var-dumper": "~2.6",
"squizlabs/php_codesniffer": "2.*"
},
"autoload": {
"psr-4": {
"ITECS\\Scholarships\\": [ "src/", "app/core/" ],
"Codeception\\Module\\": "src/",
"Tests\\Substitute\\": "tests/_helpers/"
}
}
}
Allows you to change DB across evironments
Gives you power to undo the change if there is a problem
$ vendor/bin/phinx create CreateUserLoginsTable
<?php
use Phinx\Migration\AbstractMigration;
class CreateUserLoginsTable extends AbstractMigration
{
/* Example One: Change */
public function change()
{
// create the table
$table = $this->table('user_logins');
$table->addColumn('user_id', 'integer')
->addColumn('created', 'datetime')
->create();
}
}
$ vendor/bin/phinx migrate
$ vendor/bin/phinx rollback
<?php
use Phinx\Migration\AbstractMigration;
class CreateUserLoginsTable extends AbstractMigration
{
/* Example Two: Up/Down with Migrate/Rollback */
public function up()
{
$table = $this->table('users');
$table->renameColumn('bio', 'biography');
}
public function down()
{
$table = $this->table('users');
$table->renameColumn('biography', 'bio');
}
}
$ vendor/bin/phinx migrate
$ vendor/bin/phinx rollback
DB connections
Base URL
Set paths to twig templates
Customize Notice messages
Set config variables for services
'/app/config.php'
<?php
return array(
/* base url for path in the site */
'app' => array(
'base_url' => sprintf('http://localhost:%s/', isset($_SERVER['SERVER_PORT'])
? $_SERVER['SERVER_PORT'] : ''),
'index_page' => 'index.php/',
'debug' => FALSE
),
/* DB configuration */
'db' => array(
'default' => array(
'hostname' => "local__server",
'port' => "3306",
'username' => "uname",
'password' => "pword",
'database' => "db_name_"
)
),
'twig' => array(
'template_path' => array(
__DIR__ . '/templates',
__DIR__ . '/templates/dashboard',
)
),
'authorization' => array(
'funding' => array('elstamey', 'person2'),
'developers' => array('elstamey'),
'coordinators' => array('person3')
),
'notice' => array(
'enabled' => false,
'type' => 'info',
'headline' => null,
'message' => null,
'syslink' => null
),
'errorHandling' => array(
'emailExceptions' => false
),
'awardLetters' => array(
'letterhead' => '_itecs/notification-samples/ncsu_coe_aa_letterhead.pdf',
'academicYears' => '2015-2016',
'respondByDate' => 'May 11, 2015',
'templates' => array(
'awardLetter' => 'layouts/letters/award-letter.twig',
'awardEmail' => 'layouts/letters/award-email-body.twig'
),
'fixedAttachments' => array(
'_itecs/notification-samples/thank_you_template.pdf'
),
'signatureImagePath' => '_uploads/tmp/deans_signature.png',
'outputPath' => '_uploads/awardLetters/'
),
'emailService' => array(
'sendLimit' => 5,
'retries' => 3,
'throttleTime' => 2, // seconds
'fromAddress' => 'Scholarships Application <engr-webmaster@ncsu.edu>',
'replyToAddress'=>'College of Engineering <engineering@ncsu.edu>'
)
);
?>
We passed this array into a $config variable that could be accessed as `$config['app']['base_url']`
Student Submitted Application
Budget Allocated To Scholarship
Award Was Given To Student
'/src' is given a namespace
namespaces are autoloaded in composer
{
"name": "itecs/scholarships",
"description": "Scholarships application for the College of Engineering.",
...
"autoload": {
"psr-4": {
"ITECS\\Scholarships\\": [ "src/", "app/core/" ],
"Codeception\\Module\\": "src/",
"Tests\\Substitute\\": "tests/_helpers/"
}
}
}
'/app/bindings.php' define container for new code and its dependencies
'/app/controllers'
new controllers for the new functionality
'/app/services.php' defined and configured twig, database, et al
'/app/bindings.php'
<?php
/* example one */
use Scholarships\Selection\Services\IlluminateDatabaseGpaService;
$container['Scholarships\Common\Services\GpaService'] = function($c) {
return new IlluminateDatabaseGpaService($c['database']);
};
'/app/bindings.php'
<?php
/* example two */
use Scholarships\Selection\ApplicantQueryService;
$container['Scholarships\Selection\ApplicantQueryService'] = function($c) {
return new ApplicantQueryService(
$c['Scholarships\StudentApplication\StudentQueryService'],
$c['Scholarships\Common\Services\ResidenciesService'],
$c['Scholarships\Common\Services\GpaService'],
$c['Scholarships\Common\Services\UnmetNeedService'],
$c['Scholarships\Common\Services\CandidateQualificationService']
);
};
<?php
class Selectionnext extends BaseController
{
public function Selectionnext()
{
parent::BaseController();
$this->scholarshipRepository = $this->container['Scholarships\Selection\Scholarship\ScholarshipRepository'];
$this->collaborationsService = $this->container['Scholarships\Selection\CollaborationsService'];
$this->committeeService = $this->container['Scholarships\Selection\CommitteeService'];
$this->authService = $this->container['Scholarships\IdentityAccess\AuthenticationService'];
$this->awardsService = $this->container['Scholarships\Selection\Scholarship\AwardsService'];
$this->events = $this->container['Scholarships\Support\Events\EventStore'];
}
private function getScholarshipDashboard($scholarshipId,$keepItLight=false)
{
$committeeMember = $this->getCommitteeMember();
$presenter = new ScholarshipDashboardPresenter(
$committeeMember,
ScholarshipId::fromString($scholarshipId),
$this->events,
$this->scholarshipRepository,
$this->container['Scholarships\Selection\ApplicantQueryService'],
$keepItLight
);
$scholDetails = $presenter->asArrayForJson();
$scholDetails['json_string'] = json_encode($scholDetails);
return $scholDetails;
}
?>
<?php
namespace ITECS\Scholarships;
use \ReflectionClass;
use \Log;
/**
* Base Application Controller Class
*
*/
class BaseController extends \Controller {
const BAD_REQUEST=400;
const FORBIDDEN=403;
const NOT_FOUND=404;
const METHOD_NOT_ALLOWED=405;
const SERVER_ERROR=500;
protected $serverResponseCodes;
protected $authenticationService;
/**
* @var Pimple\Container
*/
protected $container = null;
/**
* Constructor
*/
public function BaseController()
{
parent::Controller();
// In Ameyrika, BaseController Contains Container
global $container;
$this->container = $container;
$this->authenticationService = $this->container['ITECS\Scholarships\IdentityAccess\AuthenticationService'];
session_start();
$this->setExceptionHandler();
log_message('debug', "Base Controller Class Initialized");
}
/**
* Fetch the current user id
*
* @return string
*/
protected function getCurrentUserIdentity()
{
return $this->authenticationService->getCurrentAuthenticatedUser();
}
/**
* Invoke twig to render content and immediately exit afterward
*
* @param String $name pathname of template to render
* @param Array
*/
protected function render($name, array $context = array())
{
// This keeps CodeIgniter from getting frisky.
ob_end_clean();
echo $this->container['twig']->render($name, $context);
exit;
}
/**
* Force a redirect with a flash notice
*
* @param string $location where to redirect to
* @param string $title a leading label for the notice
* @param string $title the main message for the notice
* @param string $type a (Twitter Bootstrap) alert type; success, info, warning, danger
*
*/
protected function redirectAwayWithFlashNotice($location, $title, $message, $type='success')
{
$this->setFlashNotice($title, $message, $type);
redirect($location);
exit;
}
/**
* Push a flash notice into the session
* Note: Simplistic. Last notice pushed wins.
*
* @param string $title a leading label for the notice
* @param string $title the main message for the notice
* @param string $type a (Twitter Bootstrap) alert type; success, info, warning, danger
*/
protected function setFlashNotice($title, $message, $type)
{
$_SESSION['flash_notice'] = array(
'type' => $type,
'title'=>$title,
'message' => $message
);
}
/**
* Render a "Serious Error" page
* e.g. $this->dieWithErrors(self::METHOD_NOT_ALLOWED, array("You go now!", "Bad Wookie!", "I am Groot!") );
* or $this->dieWithErrors(self::BAD_REQUEST, "You not have done that.")
* or $this->dieWithErrors(self::FORBIDDEN)
*
* @param int $statusCode one of the codes we actually use: 400 (Bad Request), 403 (Forbiddeni), 404 (Not Found), 405 (Method Not Allowed), 500 (Server Error)
* @param mixed $messages a string or array of error messages; optionally an integer code or other string-interpolatable object
*/
protected function dieWithErrors($statusCode,$exception=NULL,$userMessages=NULL)
{
if (is_null($userMessages)) {
$userMessages=array();
} else if (is_string($userMessages)) {
$userMessages=array($userMessages);
} else if (!is_array($userMessages)) {
$userMessages=array("An unexpected error occurred: {$userMessages}");
}
if (!in_array($statusCode, array(self::BAD_REQUEST, self::FORBIDDEN, self::NOT_FOUND, self::METHOD_NOT_ALLOWED, self::SERVER_ERROR))) {
$statusCode = self::SERVER_ERROR;
}
$constants = new ReflectionClass('ITECS\Scholarships\BaseController');
$this->serverResponseCodes = array_flip($constants->getConstants());
$diagnosticMessages = array();
if (!is_null($exception)) {
$diagnosticMessages = array($exception->getMessage());
if ($statusCode == self::SERVER_ERROR) {
$this->reportUnhandledException($exception);
}
}
$this->render("error/{$statusCode}.twig", array(
'diagnosticMessages' => $diagnosticMessages,
'userErrorMessages' => $userMessages,
'currentUser' => ( $statusCode == 403 ? array('name'=>'Access Denied') : array('name'=>ucwords(strtolower(str_replace('_', ' ', $this->serverResponseCodes[$statusCode])))) ),
'currentUrlEncoded' => $current_url = str_replace('+','%20', urlencode(sprintf(
'Scholarships problem at %s://%s/%s',
isset($_SERVER['HTTPS']) ? 'https' : 'http',
$_SERVER['HTTP_HOST'],
$_SERVER['REQUEST_URI']
)))
));
exit;
}
/**
* General exception handler; Guards against direct URL access
*
* @param Exception $exception
* @access private
*/
public function handleUncaughtExceptions($exception)
{
$caller = debug_backtrace();
$caller = $caller[1]['function'];
if (!empty($caller)) {
$this->dieWithErrors(self::NOT_FOUND);
}
$this->dieWithErrors(self::SERVER_ERROR,$exception);
}
/**
* Set a default exception handler
*/
protected function setExceptionHandler()
{
set_exception_handler(array($this, "handleUncaughtExceptions"));
}
/**
* Send an email notice to the team that an unhandled exception was detected
* @param Exception $exception
*/
protected function reportUnhandledException($exception)
{
if ( $this->container['config']['errorHandling']['emailExceptions'] ) {
$mail_body = "An unhandled exception was detected in Scholarships.";
$mail_body .= "\n\nThe exception code was {$exception->getCode()}\n\nThe exception message was:\n{$exception->getMessage()}\n\nThe current user was '{$this->getCurrentUserIdentity()}'.\n\n";
$mail_body .= "Request URI: {$_SERVER['REQUEST_URI']}\n\n";
mail(
$recipient = 'engr-webmaster@ncsu.edu',
$subject = '[Scholarships] Unhandled Exception on ' . date("F j, Y, g:i a"),
$mail_body,
$header = "From: Scholarships Application \r\n"
);
}
Log::error($exception, array('login' => $this->getCurrentUserIdentity()));
}
/**
* Check if request HTTP method is POST
*
* @return bool
*/
protected function requestMethodIsPost()
{
return $this->container['request']->getMethod() === 'POST';
}
/**
* Guard against access to a controller action except through POST method
*
* @param mixed $message string or array of strings indicating cause of failure
* @access private
*/
protected function methodMustBePost($message)
{
if ( !$this->requestMethodIsPost() ) {
$this->dieWithErrors(self::METHOD_NOT_ALLOWED, NULL, $message);
}
}
}
By May 2015: Approximately $1,074,394 Awarded
Tight deadlines with un-scoped work, we created technical debt that we would have to address in the next academic year
Emily Stamey
Twitter: @elstamey
Blog with Bootstrapping details: elstamey.com