Detta dokument beskriver regler för hur programmering, databashantering etc ska gå till när vi utvecklar butiksystemet.
För system som byggs utanför butiksystemet gäller andra regler enligt nedan.
Dokumentet skrivs av Oskar Å. Säg gärna till om något är otydligt eller fattas!
Projekt utanför butiksystemet
För större projekt ska Symfony användas i senaste LTS-version.
För mindre projekt, använd Slim med utvalda komponenter från Symfony.
Twig används för frontend och PSR som PHP-standard.
Namnsätt inte något med Wikinggruppen / WGR i dessa projekt -
vi vill låta det vara möjligt för andra firmor att ta över dem.
Det finns fler riktlinjer för externa projekt i Wikinggruppens Google Drive.
Exempelprojekt i Symfony:
Jaktrapport
Exempelprojekt i Slim:
WGR Newsfeed
Butiksystemet är byggt i PHP i en variant av strukturen Model-View Controller (MVC). Upplägget ser något annorlunda ut i administrationen (kallad backend) och butikens "framsida" (kallad frontend).
Varje kund som använder butiksystemet har en egen kopia av all kod och databas. Då vi har som USP att all kod ska kunna skräddarsys, kan vi sällan automatiskt uppdatera alla kunders kodbaser med nya funktioner och ändringar. Det innebär att koden ser olika ut beroende på vilken version butiksystemet har.
Butiksystemet följer inte alla regler i detta dokument då reglerna arbetats fram efter att butiksystemet byggdes. Ambitionen är dock att reglerna implementeras stegvis i butiksystemet samt att de gäller för all ny utveckling.
För att kunna hantera buggfixar använder vi ett system som kan patcha alla kodbaser (filtrerat på version). Det innebär att vi skickar instruktioner till respektive kodbas för att bland annat göra en "replace". Patchmottagaren söker då efter X i en fil och ersätter med Y. På grund av detta upplägg ger varje differens i filerna en kostnad - att det blir svårare att hålla reda på vad som ska ersättas (X) i en patchning. Detta gäller både ny utveckling samt ändringar i äldre system. Därför är det inte alltid en bra idé att "rätta till" mindre viktiga saker i äldre kodbaser eftersom det då blir svårare att hitta och ersätta via patchningen. Patchverktyget används även så gott det går för att (försöka) lägga in nya funktioner och hjälpklasser.
Butiken har en developer-bar som bara visas för utvalda IP-nummer. Via den kan man aktivera utvecklarläge/produktionsläge. Utvecklarläge innebär att css- och javascriptfiler laddas var för sig med slumptal i sökvägen för att undvika cachening. Produktionsläge innbär att css- och javascriptfiler bakas ihop till färre filer (färre anrop för klienten). Via "Övrigt -> Critical CSS" aktiveras ett läge som visar hur sidan ser ut vid första anropet till servern. Via "Övrigt -> Avaktivera HTML minifiering" stänger du tillfälligt av HTML minifieringen (men bara för din webbläsare).
PHP-klasserna ska kategoriseras i olika lager: Model, View, Controller.
I butiksystemet finns speciella klasser för M, V och C som följer en hierarisk struktur med tre nivåer: Shop, Backend/Frontend och Page. Page-nivån ärver från Backend/Frontend som ärver från Shop. Till exempel har ShopModel tillgång till databasen, vilket därmed görs tillgängligt inom en "PageModel" också.
I backend/butikadmin anropas en php-fil per sida där filen innehåller page-klasser för M,V och C (i vissa fall ligger M i egen fil om den delas av olika sidor i backend).
I frontend sker alla anrop genom en generell FrontendController som tolkar den URL klienten anropar och laddar rätt Page-MVC (i /frontend).
Exempel på fil som innehåller de tre klasstyperna på "page-nivå":
/**
* Example page-level model class
*/
class WGR_ExamplePageModel extends WGR_BackendModel
{
/** @var array */
public $data;
/**
* Loads some example data
* @param int $id
*/
public function loadData(int $id)
{
$standaloneModel = new WGR_StandaloneModel($this);
$this->data = $standaloneModel->getData($id);
}
}
/**
* Example page-level view class
*/
class WGR_ExamplePageView extends WGR_BackendView
{
/**
* Renders the page
* @param WGR_ExamplePageModel $pageModel
*/
public function render(WGR_ExamplePageModel $pageModel)
{
// renderView used from WGR_BackendView
// template.php will get access to $pageModel
$this->renderView($pageModel, 'template.php');
}
}
/**
* Example page-level controller class
*/
class WGR_ExamplePageController extends WGR_BackendController
{
/**
* Controller method for the page
*/
public function execute()
{
$pageModel = new WGR_ExamplePageModel();
$pageView = new WGR_ExamplePageView();
switch ($pageModel->GET('action')) {
case 'showTitle':
$pageModel->loadData((int) $pageModel->GET('id'));
$pageView->render($pageModel);
break;
}
}
}
$controller = new ThePageController();
$controller->execute();
Vi ska dock se PageModel-klasserna främst som behållare av data för det som behövs för åtkomst från en PageView. Den mesta databehandlingen ska ligga i en fristående Model-klass som inte ärver tillgång till databasen och liknande (som PageModel-klasser gör). På det viset kan en sådan klass bli användbar från både frontend och backend samt från andra Model-klasser utan problem. Sådana klasser kan liknas vid Services i andra ramverk såsom Symfony. Istället för att ha tillgång till databasen via arv, tar de in en referens till en ShopModel via sin konstruktor. Denna referens ska sparas lokalt i klassen och heta $model. Exempel nedan:
/**
* Example standalone model
*/
class WGR_StandaloneModel
{
/** @var WGR_ShopModel */
private $model;
/**
* @param WGR_ShopModel $model
*/
public function __construct(WGR_ShopModel $model)
{
$this->model = $model;
}
/**
* @param int $id
* @return array
*/
public function getData(int $id)
{
return $this->model->dbFetchPrepared(
'SELECT title_sv, description_sv
FROM products
WHERE id = ?',
array($id),
PDO::FETCH_ASSOC
);
}
}
Här listas några av de vanligaste hjälpfunktionerna.
En del funktioner finns inte i äldre versioner av butiksystemet - då går det bra att hämta dem från en nyare version.
I klassen WGR_ShopModel finns GET, POST, COOKIE
som gör det smidigt att läsa från anropets $_GET etc då de kollar isset och har defaultvärden.
Om du ska behandla relativt många värden från ett anrop är det bättre att använda en instans av
WGR_ParameterBag vilken finns med sedan v10.
Däremot om det handlar om enstaka värden från anropet så bör Controller-klassen läsa värdet och skicka in värdet till Model via funktionsanropet.
I klassen WGR_ShopView finns safeOutput vilken ska användas i viewfiler
för att förhindra rendering av html/script injections av data från databasen.
I klassen WGR_DatabaseModel (som ärvs av WGR_ShopModel) finns hjälpfunktioner för databasen samt direktaccess till db-objektet (en PDO-instans).
Använd alltid en "prepared" SQL om körningen kräver dynamisk indata, då är det skyddat mot SQL injections.
dbExecuteSimple = SQL som inte ger resultat (osäker)
dbExecutePrepared = SQL som inte ger resultat (säker, skyddar mot SQL injections)
dbFetchSimple, dbFetchPrepared = Hämtar en rad eller null
dbFetchAllSimple, dbFetchAllPrepared = Hämtar alla rader eller null
dbCheckEmptySimple, dbCheckEmptyPrepared = Kollar om en SQL ger 0 st rader
dbBeginTransaction, dbCommitTransaction, dbRollbackTransaction = Används för att hantera transactions i MySQL.
För kontroll och behandling av uppladdade bilder använd WGR_UploadModel.
Andra typer av uppladdade filer kan behandlas med hjälp av WGR_FileModel.
Kryptering ska ske med WGR_EncryptionModel men signering och lösenordshantering ska ske av WGR_SignerModel.
För att undvika att script körs av olika processer samtidigt använder vi WGR_LockModel som skapar ett file mutex för
att tillåta en process i taget att köra en del av ett script.
Http-anrop ska göras med WGR_HttpClientModel istället för att använda curl direkt.
När databasen returnerar flera värden per rad hanterar vi det vanligtvis som stdClass pga parametern PDO::FETCH_OBJ i databasanropet. JSON kan avkodas som antingen array eller objekt. Det är även möjligt att använda klasser, vilka ska läggas i enskilda filer under /shared/model/object. Sådana objektklasser är främst behållare av data men kan innehålla funktioner som behandlar sin egna data, men inte funktioner som beror på andra klasser. Objektklasser ska användas om samma datastruktur ska hanteras av flera klasser. Exempel: WGR_Address i adress.php.
// Bad
public function b($c,$j=0)
{
$a=$c;
for($i=$j;$i<10;$i+=2){
if($i++<5){
$a=$i.$a;
}
}
}
// Good
public function b($c, $j = 0)
{
$a = $c;
for ($i = $j; $i < 10; $i += 2) {
if ($i++ < 5) {
$a = $i . $a;
}
}
}// Good
$price = $isCampaign ? $campaignPrice : $ordinaryPrice;
// Bad - too complicated!
$price = ($isCampaign && $customerType == 'private')
|| $forceCampaignPrices ?
($currency == 'NOK' ? $campaignPriceNOK : $campaignPriceSEK) :
($currency == 'SEK' ? $ordinaryPriceNOK : $ordinaryPriceSEK);
// Good
if ($action === 'save')
// Bad
if ($action == 'save')
// Good
if ($isVisible)
// Bad
if ($isVisible == 0)
// Good
if ($cellphone)
// Bad (it could be null)
if ($cellphone !== '')// Bad
if ($checkOne) {
if ($checkTwo) {
return true;
}
else {
return false;
}
}
else {
return false;
}
// Good
if (!$checkOne) {
return false;
}
if (!$checkTwo) {
return false;
}
return true;
// Bad
foreach ($rows as $row) {
if ($row->checkOne) {
if ($row->checkTwo) {
$sum += $row->amount;
}
}
}
// Good
foreach ($rows as $row) {
if (!$row->checkOne) {
continue;
}
if (!$row->checkTwo) {
continue;
}
$sum += $row->amount;
}
/**
* Includes class definitions for ExampleClass
*/
/**
* This is an example class
*/
class WGR_ExampleClass
{
/**
* This is an example function
* @param string $a Possible values: apelsin / banan / citron
* @param int $b Value to be transformed
* @return int Transformed value
*/
public function exampleFunction(int $a, int $b) : int
{
switch ($a) {
case 'apelsin':
$b *= 2;
break;
case 'banan':
$b += 3;
break;
case 'citron':
// No need for further processing, citron is always 5!
return 5;
break;
}
// Wide array
$fruits = [
'apelsin',
'banan',
'citron',
];
// Chained commands
$request = \ExternalPaymentProvider\WebPayAdmin::queryOrder($this->config)
->setOrderId($transactionId)
->setCountryCode($order->shippingAddress->countryCode)
->setFruits($fruits)
->queryInvoiceOrder();
// Long if
if ($request->getNumberOfApples() > 50
&& $request->getNumberOfBananas() > 60
&& $request->getNumberOfCoconuts() > 70
) {
return 50;
}
elseif ($request->getNumberOfApples() < 30) {
return $request->getNumberOfDogs();
}
// No need for "else" here
return 1;
}
}
Alla filer som behandlar ett request ska inkludera /environment.php som i sin tur inkluderar /../vendor/autoload.php från Composer.
Environment.php aktiverar även errorhantering och ställer in config för databasanslutningen.
Paketen från Composer använder oftast namespaces.
I dessa fall anger vi vilka namespaces vi använder med use i klasserna som använder dem.
Innan version 10 har vi historiskt inte haft någon autoloader utan istället använt require_once
för att läsa in egna klasser. Det är inte längre nödvändigt tack vare vår egna autoloader i
WGR_AutoloadModel, som automatiskt hittar och cachar sökvägar till alla klasser
med namn som börjar med "WGR_". Om andra klasser, t.ex. från olika SDK ska användas behöver de inkluderas
med require_once om de inte laddas med Composer.
Obs: Det är mycket viktigt att inte lämna några *.php-filer med kopior/backup av klasser eftersom
vår autoloader kan välja dessa sökvägar istället för den rätta klassfilen.
Undvik långa funktioner. Om en funktion utför flera olika uppgifter ska den brytas upp den i fler funktioner. Sikta på en maxlängd på 100 rader per funktion. Tänk gärna utifrån ett perspektiv av testning: testbara funktioner måste vara relativt korta och utföra enskilda uppgifter.
Undvik att omsluta stora kodblock i if, else eller try. Var mer specifik istället. Istället för att t.ex. ha en if-sats inuti en for-loop där det händer saker inuti if-satsen, kan en kortare if-sats köra "continue" istället ovanför detta kodblock. På samma sätt kan en hel funktion avbrytas tidigt med en kort if-sats istället för att omsluta hela funktionskroppen av en jättestor if-sats. Try-satser ska alltid vara så snäva som möjligt. Se även avsnittet om "tidig return" under Syntax.
echo '<strong>', $title, '</strong>';
<div class="outer">
<?php
if ($a) {
// Utskrift av HTML i if-sats
?>
<strong>AAA</strong>
<?php
}
elseif ($b) {
// Utskrift av värde och tecken (paranteser)
?>
(<?= $b ?>)
<?php
}
else {
// Utskrift av endast PHP-värde
echo $a;
}
?>
<div class="inner">
<h1><?= $title ?></h1>
<ul>
<?php
foreach ($rows as $row) {
?>
<li><?= $row->title ?></li>
<li><?= $row->author ?></li>
<?php
}
?>
</ul>
</div>
</div>
PHP-koden ska fungera i både PHP-version 7.4 och 7.3.
Ha som mål att modulen ska vara enkel att installera i andra butiker.
Därmed ska din kod hållas isolerad/inkapslad och separerad snarare än nästlad
med standardkoden, så gott det går. Skapa nya klasser i /shared/model/ (se WGR_StandaloneModel ovan)
där den mesta koden läggs, och anropa sedan din class från cartModel
istället för att bygga in all din kod i cartModel (exempelvis).
Ju mindre vi påverkar standardkoden desto lättare blir den att buggfixa,
dessutom blir koden mindre rörig med tiden om vi håller modulerna
separerade.
Gör det enkelt att stänga av modulen, genom att lägga till ett configvärde specterIsActive
som går att kryssa ur i wgrtools och som blockerar tillgång, cronjob etc till modulen.
Om du vill skapa en ny cookie, tänk på att butiksystemet kanske använder en modul för cookiehantering där icke-nödvändiga cookies blockeras automatiskt. Vår egna modul har en kommaseparerad lista i config.cookieControlNecessaryCookies där cookienamnet måste läggas in för att cookien ska godkännas automatiskt.
Cronjob ska om möjligt köras via CLI genom filen /bin/command.php vilket har fördelen att Apache/Cloudflare/etc inte kan bestämma timeout, samt att anropet loggas till Graylog. Se bubbles för exempel på hur scriptet kan användas.
<!-- Dåligt -->
<style>
button {
color: green;
}
</style>
...
<button style="font-weight: bold;" onclick="return submit()">Submit</button>
...
<script>
function submit() { }
</script>
<!-- Bra -->
<!-- style.css -->
button {
color: green;
font-weight: bold;
}
<!-- index.html -->
<button id="js-submit">Submit</button>
<!-- jquery.js och script.js -->
$('#js-submit').on('click', submit);
function submit() { }
<!-- Dåligt -->
<SPAN>Min andra <br/> text</SPAN>
<p>Min text
<img CLASS="img" src="robin.jpg" width=50 height=50 />
<!-- Bra -->
<span>Min andra <br> text</span>
<p>Min text</p>
<img class="img" src="robin.jpg" width="50" height="50">
<div>
<!-- Långt kodblock -->
</div> <!-- End .l-holder -->
<!-- Dåligt -->
<input type="checkbox" disabled="disabled" checked="checked">
<!-- Bra -->
<input type="checkbox" disabled checked>
/**
* A example module.
* @module myModule
*/
(function(WGR, $) {
'use strict';
const publicConst = 'public';
const privateConst = 'private';
/**
* My special function.
* @param {{id: number, isValid: boolean}} options - My special function options.
* @returns {string}
*/
function myFunc(options)
{
...
let myLet = false;
if (options.isValid) {
myLet = true;
}
...
}
WGR.myModule = {
publicConst: publicConst,
myFunc: myFunc
}
}(window.WGR = window.WGR || {}, jQuery));
// Hoisting
console.log(variable); // undefined
var variable = 'My variable';
console.log(letVariable); // ReferenceError
let letVariable = 'My let variable';
console.log(constVariable); // ReferenceError
const constVariable = 'My const variable';
// Block scope
if (true) {
var variable = 'My variable';
}
variable; // My variable
if (true) {
let letVariable = 'My let variable';
const constVariable = 'My const variable';
}
letVariable; // ReferenceError
constVariable; // ReferenceError
// Exempel för global variabel:
let myGlobalVariable = true; // Fel
WGR.myGlobalVariable = true; // Rätt
// Examples
/**
* Fetches a phrase from the compiled phrase file.
* @param {string} phraseName - Phrase name.
* @param {string|string[]} [params=[]] - Input data for the phrase.
* @returns {string}
*/
function getPhrase(phraseName, params) {
// [...]
}
/**
* Add item to cart.
* @param {{id: number, name: string}} item - The item to add to cart.
* @returns {Number}
*/
function addItem(item) {
// [...]
}
/**
* Find container for area.
* @param {Object} container - The container to find.
* @param {number} container.id - The container id.
* @param {string} container.name - The name of the container.
* @param {string} container.createdOn - When then container was created.
* @returns {HTMLElement}
*/
function findContainerForArea(container) {
// [...]
}
/**
* A module that sums numbers.
* @module specialModule
*/
(function(WGR, $) {
'use strict';
const prefix = 'Total: ';
/**
* Returns the sum of a and b
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b)
{
return a + b;
}
/**
* Returns sum of a and b with prefix
* @param {number} a
* @param {number} b
* @returns {string}
*/
function getNumber(a, b)
{
return prefix + add(a, b);
}
WGR.specialModule = {
getNumber: getNumber,
};
}(window.WGR = window.WGR || {}, jQuery));
WGR.specialModule.getNumber(2,3); // Total: 5
// Simulate click event
const element = {
onClick: function(fn) {
return fn();
}
}
/**
* Factory
* @param {string} username
* @returns {{ getUsername: function }}
*/
function createUser(username)
{
const usernamePrefix = 'Username: ';
return {
getUsername: function() {
return usernamePrefix + username;
}
}
}
const user1 = createUser('User1');
user1.getUsername(); // 'Username: User1'
element.onClick(user1.getUsername); // 'Username: User1'
user1.usernamePrefix; // undefined
/**
* Prototype
*/
function User(username)
{
this.username = username;
this.usernamePrefix = 'Username: ';
}
User.prototype.getUsername = function() {
return this.usernamePrefix + this.username;
}
const user1 = new User('User1');
user1.getUsername(); // 'Username: User1'
element.onClick(user1.getUsername); // NaN
user1.usernamePrefix; // 'Username: '
/**
* Class
*/
class User
{
constructor(username) {
this.username = username;
this.usernamePrefix = 'Username: ';
}
getUsername() {
return this.usernamePrefix + this.username;
}
}
const user1 = new User('User1');
user1.getUsername(); // 'Username: User1'
element.onClick(user1.getUsername); // NaN
user1.usernamePrefix; // 'Username: '
// Dåligt (namnet är låst till en viss färg)
.btn--blue {
background-color: #f00;
}
// Bra
.btn--primary {
background-color: #f00;
}
.selector-1,
.selector-2,
.selector-3[type="text"] {
display: block;
margin: 0 auto;
background: #fff;
background: linear-gradient(#fff, rgba(0, 0, 0, 0.8));
color: #333;
font-family: "Open Sans", arial, sans-serif;
}
.selector-a,
.selector-b {
padding-top: 10px;
padding-bottom: 10px;
}
.selector {
// Positioning
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 10;
// Flex
display: flex;
align-items: center;
justify-content: center;
// Display & Box Model
border: 10px solid #333;
box-sizing: border-box;
height: 100px;
margin: 10px;
overflow: hidden;
padding: 10px;
width: 100px;
// Other
background: #000;
color: #fff;
font-family: sans-serif;
font-size: 1rem;
text-align: right;
}
//
// Block
//
// Basic comment
#holder button {
border: 1px solid #000;
}
.form button {
border: 1px solid #fff;
}
// Button kommer få en svart border eftersom ID har högre specificitet än en klass.
// Dåligt
ul.nav {
display: inline-block;
}
// Bra
.nav {
display: inline-block;
}
// Undvik
.nav {
display: inline-block;
li {
list-style-type: none;
}
}
// Bättre
.nav {
display: inline-block;
> li {
list-style-type: none;
}
}
html {
font-size: 1rem;
line-height: 1.45;
}
.btn {
border: 1px solid #222;
background-color: #fff;
}
.is-btn-active {
background-color: #222;
}
// Dåligt (hög specificitet)
#third-party-widget {
}
// Bättre (låg specificitet)
[id="third-party-widget"] {
}
Vi använder oss av följande CSS struktur: Base, Layout, Modules, States, Themes.
// Base
body {
background-color: #fff;
}
// Layout
.l-holder {
max-width: 1150px;
}
// Module
.btn {
font-size: 10px;
}
// Module (modifier)
.btn--large {
padding: 10px 14px;
font-size: 20px;
}
// State
.is-hidden {
display: none !important;
}
// Theme
.btn {
background-color: #000;
}
BEM står för block, element, modifier och är en metod för namnsättning av komponenter på webben. Vi använder BEM för våra CSS moduler (modules).
// Block (module)
product-item {
position: relative;
}
// Element
.product-item__heading {
height: 35px;
color: #000;
font-size: 13px;
line-height: 1.3;
}
// Modifier
.product-item__heading--campaign {
color: #f00;
}
// HTML
<div class="product-item">
<h2 class="product-item__heading product-item__heading--campaign">Item 1</h2>
</div>
LESS är en CSS preprocessor som bygger ut grundfunktionaliteten i CSS med variabler, mixins, nesting och funktioner vilket gör det lättare att underhålla, skapa teman och bygga ut butiken/hemsidan.
// Dåligt
.l-holder {
position: relative;
max-width: @page-width;
margin-left: auto;
margin-right: auto;
}
...
@media @page {
...
.l-holder {
margin-left: @page-gutter;
margin-right: @page-gutter;
}
}
...
@media @tablets {
...
.l-holder {
margin-top: 15px;
}
}
// Bra
.l-holder {
position: relative;
max-width: @page-width;
margin-left: auto;
margin-right: auto;
@media @page {
margin-left: @page-gutter;
margin-right: @page-gutter;
}
@media @tablets {
margin-top: 15px;
}
}
Vi ansluter till MySQL genom PHP-paketet PDO. Använd PDO's egna konstanter för att välja resultatyp. Oftast används PDO::FETCH_OBJ eller PDO::FETCH_COLUMN.
Ska samma SQL-sats köras flera gånger i en loop med olika indata per iteration? Använd då ett "prepared statement". Databasen tolkar & kompilerar då frågan 1 gång men exekverar den flera gånger. Exempel:
$dbStatement = $this->db->prepare(
'INSERT INTO foo (bar)
VALUES (?)'
);
while (1) {
$dbStatement->execute(array('hej'));
}
Vi ska spara rådata i databasen (genom "prepared statements") istället för att t.ex. byta ut citationstecken och script-taggar innan.
Risken finns då att någon sparar ett javascript i databasen som körs när värdet skrivs ut.
Scriptet kan då t.ex. kopiera en administratörs cookie och skicka iväg den. Därför är det viktigt att vid
utskrift alltid använda t.ex. hjälpfunktionen safeOutput som gör om text till html-entities.
Även om vi vet att indata till en SQL är riktiga integers så ska vi använda prepare-funktionerna
då det dels blir en vanesak att använda säkra metoder, dels bättre syntax highlighting i editorerna
när SQL-strängen inte behöver brytas. Simple-funktionerna bör därmed bara användas om SQL-satsen
inte har någon dynamisk indata.
Skriv SQL med varje "SQL-ord" i versaler. Låt varje logiskt avsnitt (FROM/JOIN/WHERE/ORDER/GROUP/LIMIT/VALUES etc) börja på egen rad.
Om raderna trots det blir långa, bryt raden och fortsätt indenterat. Följ detta även för korta SQL-satser.
Om det hämtas värden från flera olika tabeller, gruppera då värdena efter tabell och gärna med en radbrytning emellan grupperna.
Undvik SELECT * eftersom det underlättar PHP-skrivande om man vet vilka kolumner som
finns tillgängliga. Undantag är dynamiska kolumner som beror på t.ex. antal aktiva språk.
Lägg ett space före paranteserna i INSERT/VALUES. Exempel:
SELECT a.name, a.hej, a.lussebulle,
b.pris, b.cykel,
c.skiva, c.armborst, c.lampa
FROM apelsin AS a
INNER JOIN banan AS b ON b.id = a.bID
INNER JOIN (
SELECT skiva, armborst, lampa
FROM citron
ORDER BY RND()
LIMIT 1
) AS c ON c.type = a.cType
SELECT id
FROM apelsin
WHERE bID = 2
DELETE a.*
FROM apelsin AS a
LEFT JOIN banan AS b ON b.id = a.bID
WHERE b.pris IS NULL
INSERT INTO apelsin (hej, du)
VALUES ('tjena', 'tja')
$dbResult = $this->dbFetchAllPrepared(
'SELECT a.id
FROM apelsin AS a
WHERE a.banan = ?',
array($banan),
PDO::FETCH_OBJ
);
$dbResult = $this->dbFetchAllPrepared(
'SELECT a.id
FROM apelsin AS a
WHERE a.banan = ?
AND (a.citron = ? OR a.citron = ? OR a.citron = ?)
AND a.damask = ?',
array(
$banan,
$citron1,
$citron2,
$citron3,
$damask,
),
PDO::FETCH_OBJ
);
$this->dbExecutePrepared(
'INSERT INTO people (firstName, lastName, streetRow1, streetRow2, zipCode, city)
VALUES (:firstName, :lastName, :streetRow1, :streetRow2, :zipCode, :city)'
array(
':firstName' => $firstName,
':lastName' => $lastName,
':streetRow1' => $streetRow1,
':streetRow2' => $streetRow2,
':zipCode' => $zipCode,
':city' => $city,
)
);
$isHidden = $this->dbFetchSimple(
'SELECT isHidden
FROM config',
PDO::FETCH_COLUMN
);
if (!$isHidden) {
// Show the item
}Det är viktigt att ha grundläggande koll på index och bra/dåliga sätt att skriva SQL-frågor då den vanligaste flaskhalsen är MySQL som får arbeta i onödan.
Index
Skapa index för allt som kan användas för sökning, sortering eller som relationsvärde till andra tabeller.
Index bör byggas med flera kolumner anpassade efter SQL som körs mot tabellen.
Om det går att ha flera kolumner i indexet är det ofta bättre än flera mindre index.
Ordningen på kolumnerna bör vara:
# Bad
WHERE DATEDIFF(NOW(), orderTime) <= 2
# Good
WHERE orderTime >= DATE_SUB(NOW(), INTERVAL 2 DAY)
# Bad
SELECT fi.id, fi.filterID, fi.title_sv AS title,
fi.imageFilename, GROUP_CONCAT(fp.productID) AS productIDs
FROM filter_items AS fi
INNER JOIN filter_products AS fp ON fp.filterItemID = fi.id
INNER JOIN products_categories AS pc ON pc.productID = fp.productID
INNER JOIN products AS p ON (p.id = fp.productID AND p.isHidden = 0)
WHERE pc.categoryID = 47
GROUP BY fi.id
ORDER BY fi.listOrder
# Good
SELECT fi.id, fi.filterID, fi.title_sv AS title, fi.imageFilename,
(
SELECT GROUP_CONCAT(productID)
FROM filter_products
WHERE filterItemID = fi.id
) AS productIDs
FROM filter_items AS fi
WHERE EXISTS (
SELECT 1
FROM products_categories AS pc
INNER JOIN products AS p ON p.id = pc.productID AND p.isHidden = 0
INNER JOIN filter_products AS fp ON fp.productID = p.id
WHERE pc.categoryID = 47 AND fp.filterItemID = fi.id
)
ORDER BY fi.listOrderDetta gäller de butiker och/eller ärenden som använder Git. Se dokumentet "Git och Gitlab" i Wikinggruppens Google Drive för mer information om Git, Gitlab och lokal utveckling.