Overview
Mindsets:
- Leave things better than you found it.
- For example by keeping the dependencies updated.
- For PHP:
composer outdated - For JS and CSS:
yarn upgrade
- For PHP:
- Also, by adding missing type information in legacy code.
- For example by keeping the dependencies updated.
- Keep it simple, always favor simplicity over complexity. Try not to over engineer.
- Strive to use descriptive names, with an affirmative meaning for variables, clases, methods, etc.
- Prefer longer and descriptive names over short ones
- While it may sound controversial, default to not using comments.
- If you think that something needs a comment, first see if the code is not yet clear enough and needs improvement.
- If the code is already clear and you feel the comment would add value, then make sure the comment explains WHY and not WHAT.
General
- Prefer single quotes
'over double quotes" - Use 4 spaces instead of tabs for all files
- Except for files like YML that require 2 spaces
- Default to strict comparisons
=== - Use camelCase for:
- Variables
- Functions and methods
- Macros
- Use CapitalizedCamelCase for:
- Classes and their file names
- Use snake_case for:
- Config vars:
config('reader.coniglio_url') - Query parameters:
/library/filter?latest_issues=latest_issues
- Config vars:
- Use kebab-case for:
- Localizations:
trans('dashboard.plan-stats.issues-stats')
- Localizations:
- Keep indentation to the minimum.
- Makes code easier to follow and reason about.
- Early returns could help with this.
- Prefer more functional loops such as
map,eachandreduce, over basic ones likeforandfoeach. - If two things work together and there's no clear reason why they can not share the same name, then it's probably a good idea to use the same name.
- If you're just returning a boolean, then opt for a ternary and leave the boolean implicit:
# Instead of this
if ($bla === $ble) {
return false;
} else {
return true;
}
# Or this
return $bla === $ble ? true : false;
# Do this
return $bla === $ble;
- Avoid nesting ternary operations at all costs, it makes code hard to understand and can lead to unexpected bugs.
- When adding code from an external source by copy/paste (like from Stack Overflow), remember to always add a comment that explains what the code does, why we need it and where you took it from.
When creating new projects
- Use the generic .editorconfig. Your text editor or IDE might need a plugin for it.
- Use the generic .gitignore. We maintain this file up to date with the patterns we most usually need.
Things to take into account:
- Take into account that this is a multilanguage platform, all string must be localizable.
CI / CD
- For new projects, always include the base GitLab CI template
- Ensure the CI passes after every push and, specially, before deploying.
PHP
- Use PSR-2.
- Always keep .env.example updated.
- Always use
rawurlencodeandrawurldecode, overurlencodeandurldecode
JavaScript
- Prefer yarn over npm.
- Prefer
letandconstovervar. Ref 1, - Prefer
importoverrequire, update legacy references if you encounter any. - Always add the file extension when importing a local file:
import cacheResources from './cacheResources.js';. - Use
map,reduceandfilterwherever possible, and try to avoid usingfor,for...in,for...ofloops. - Use the entire word
errorwhen trackign errors client side:
try {
// ...
} catch (error) {
logger.error('Error doing...', { error });
// It's the same as logger.error('Error doing...', { error: error });
}
CSS
- Think twice before using
!important. It's almost always better to hack your way out of a specificity problem. When in doubt, refer to this handy guide.
Vue.js
- Prefer single file components
- The
<style>element must be SCSS and scoped, like this:<style lang="scss" scoped> -
- Clear event listeners on component destroy with $off, source
- Always use kebab-case for event names, source
- Always use :key in v-for loops, source
- Use PascalCase or kebab-case for single file components, source
- Use prefix for Base Component names, source
- Use PascalCase for component names in JS, source
- Prop names should always use camelCase during declaration, but kebab-case in templates, source
- Use Component options order from style guide, source
- Never use v-if on the same element as v-for, source
- For components file name we use CapitalizedCamelCase, the same for all uses in other Vue components.
// Instead of this
components: {
'export-button': require('../../reusable-components/ExportButton.vue').default,
},
// Do this
import ExportButton from '../../reusable-components/ExportButton.vue';
components: {
'ExportButton',
},
- When working with dynamic CSS clases and variables, always attempt to use the exact same word, or combination of words, for the class name and the variable name.
// Instead of this
<div class="DownloadButton" :class="{ downloaded, 'download-disabled': disabledButton }" v-if="canDownload">
// Do this
<div class="DownloadButton" :class="{ downloaded, 'disabled': disabled }" v-if="canDownload">
// Which can also be written as
<div class="DownloadButton" :class="{ downloaded, disabled }" v-if="canDownload">
Laravel
- Migrations move forward, except strictly required there's no need to use the
down()method in migrations. - Always put the
tenant_idcolumn in the first place. - Always use Laravel constructs to access data normally inside PHP global variables like
$_POST,$_GET,$_SERVER,$_REQUESTand$_COOKIES. - Always validate request's data before using or storing it, we cannot trust what happens outside the app. It prevent us from 2 main scenarios:
- Our clients (web frontend, mobile app) misbehaving and submiting broken data.
- Malicious actors trying to hack in or just cause damage.
- When creating a new controller, default to also creating a form request to encapsulate authorization and validation.
- The same goes for refactoring old code, try to migrate the validation and authorization to a form request. Adhering to our principle of leaving things better than you found them.
- Never use
$request->all()apart from logging it or validation, we never knwo what does the request contain. There's safer alternatives like:$request->only('...', '....').$request->validated().$validated = $request->validate(['...'])and then use$validated.
- If you add something to a column in
tenants_meta(or any other column with a JSON data type), make sure to also add a migration that makes that change in all rows. - If the Controller only has 1 public method (like an ExportUsersCsvHandler case), the we preferably use an
__invoketype of method and name the Controller as a Handler. - If possible, use
whereBetweeninstead Eloquent'swhereYear,whereMonthorwhereDaymethods. The performance difference is outrageous becausewhereBetweencan use the index of a date column, the others can't.
# Instead of this:
->whereYear('publication_date', 2019)
# Do this:
->whereBetween('publication_date', [
new Carbon('first day of jan 2019'),
new Carbon('last day of dec 2019'),
])
- Use Collections wherever possible and try to avoid using
forandfoeachloops - Prefer global helper functions over Facades. For example
logger()overLog::info()andcache()->get()overCache::get(), but 👇 - Prefer container injection over everything else.
# Instead of this:
public function index()
{
request()
...
}
# Do this:
public function index(Request $request)
{
$request
...;
}
- When in need of a Carbon object, prefer the global
now()helper instead ofCarbon::now(). - In farfalla, if possible use the
tenant()global helper to access tenant data. Eg:tenant('id')instead ofapp('CurrentTenant')->get()->id - There's no need to explicitly define the
: Responsereturn type in Controller methods. In a Controller where we only use the default rest methods, it's assumed that we're returning a response. - If you're returnig a redirect, do it like this:
# Instead of this
return redirect()
# Do this
return redirect()->to('https://...')
# Or if you're returning a route
return redirect()->to(route('login'))
- If you're returnig a redirect with an intended url:
# Instead of this to put the url
Session::put('url.intended', $request->fullUrl());
# Do this
redirect()->setIntendedUrl($request->fullUrl());
# And get it as this
return redirect($request->session()->get('url.intended'));
- If you need to make a request to an external service, try to always use Zttp. If that's not enough, use Guzzle. Manually managing
curlis the last resource. This is because we favor readability and practicality. - Using
DB::rawor similar raw database queries is always a last resource and must be justifiable, either by performance, practicality or just because there's simple no other alternative. This is not only because we favor readability and practicality, but also because using raw DB queries can get complicated and lead to security issues that are simply not possible when using the ORM. - Name cookies in
hyphen-case. - Use
$request->boolean('is_admin');to retrieve boolean properties from the request. Theboolean('...')method returnstruefor 1, "1", true, "true", "on", and "yes". All other values will returnfalse: - Use the entire word
exceptionwhen trackign exceptions server side:
try {
# ...
} catch (Exception $exception) {
logger()->error('Exception doing...', [
'exception' => $exception,
]);
}
Testing
To keep in mind when testing
- Test should be easy to follow and quick to understan, just like regular code. Try to avoid generating complex scenarios if it's not absolutely necesary.
- Preferably, use dynamic random data. For example by using Faker.
- All tests will run on tenant 1 by default:
- farfalla.test in local env.
- stagingapp.publica.la in staging.
- app.publica.la in production.
- We are using the same MySQL database as the app uses for regular requests, we create a transaction and rollback for every test.
- It wil use the default
.envfile, it could also override variables in phpunit.xml. - Use
/** @test */to indicate tests from PHPUnit. - Use camel_case test name, like
reuses_user_if_emails_match_and_uuid_is_nulland do not use thetestprefrix.